tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from dateutil.tz import tzlocal 41from time import sleep 42 43import re 44import json 45import requests 46import traceback as tb 47from typing import Union 48 49from multiprocessing import cpu_count, Lock 50from multiprocessing.pool import ThreadPool 51import pandas as pd 52 53from mako.template import Template # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 54from Templates import * # Some html-templates used by reporting methods in TKSBrokerAPI module 55from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 56from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 57 58from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator) 59from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 60 61import UniLogger as uLog # Logger for TKSBrokerAPI 62 63 64# --- Common technical parameters: 65 66PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 67uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 68uLogger.level = 10 # debug level by default for TKSBrokerAPI module 69uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 70 71__version__ = "1.6" # The "major.minor" version setup here, but build number define at the build-server only 72 73CPU_COUNT = cpu_count() # host's real CPU count 74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 75 76 77class TinkoffBrokerServer: 78 """ 79 This class implements methods to work with Tinkoff broker server. 80 81 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 82 83 About `token`: https://tinkoff.github.io/investAPI/token/ 84 """ 85 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 86 """ 87 Main class init. 88 89 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 90 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 91 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 92 :param useCache: use default cache file with raw data to use instead of `iList`. 93 True by default. Cache is auto-update if new day has come. 94 If you don't want to use cache and always updates raw data then set `useCache=False`. 95 :param defaultCache: path to default cache file. `dump.json` by default. 96 """ 97 if token is None or not token: 98 try: 99 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 100 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 101 102 except KeyError: 103 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 104 raise Exception("Token required") 105 106 else: 107 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 108 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 109 110 if accountId is None or not accountId: 111 try: 112 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 113 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 114 115 except KeyError: 116 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 117 118 else: 119 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 120 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 121 122 self.version = __version__ # duplicate here used TKSBrokerAPI main version 123 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 124 125 Latest version: https://pypi.org/project/tksbrokerapi/ 126 """ 127 128 self.__lock = Lock() # initialize multiprocessing mutex lock 129 130 self.aliases = TKS_TICKER_ALIASES 131 """Some aliases instead official tickers. 132 133 See also: `TKSEnums.TKS_TICKER_ALIASES` 134 """ 135 136 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 137 138 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 139 140 self._ticker = "" 141 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 142 143 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 144 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 145 146 See also: `SearchByTicker()`, `SearchInstruments()`. 147 """ 148 149 self._figi = "" 150 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 151 152 See also: `SearchByFIGI()`, `SearchInstruments()`. 153 """ 154 155 self.depth = 1 156 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 157 158 See also: `GetCurrentPrices()`. 159 """ 160 161 self.server = r"https://invest-public-api.tinkoff.ru/rest" 162 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 163 164 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 165 """ 166 167 uLogger.debug("Broker API server: {}".format(self.server)) 168 169 self.timeout = 15 170 """Server operations timeout in seconds. Default: `15`. 171 172 See also: `SendAPIRequest()`. 173 """ 174 175 self.headers = { 176 "Content-Type": "application/json", 177 "accept": "application/json", 178 "Authorization": "Bearer {}".format(self.token), 179 "x-app-name": "Tim55667757.TKSBrokerAPI", 180 } 181 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 182 183 See also: `SendAPIRequest()`. 184 """ 185 186 self.body = None 187 """Request body which send to broker server. Default: `None`. 188 189 See also: `SendAPIRequest()`. 190 """ 191 192 self.moreDebug = False 193 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 194 195 self.useHTMLReports = False 196 """ 197 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 198 199 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 200 """ 201 202 self.historyFile = None 203 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 204 205 See also: `History()`. 206 """ 207 208 self.htmlHistoryFile = "index.html" 209 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 210 211 See also: `ShowHistoryChart()`. 212 """ 213 214 self.instrumentsFile = "instruments.md" 215 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 216 217 See also: `ShowInstrumentsInfo()`. 218 """ 219 220 self.searchResultsFile = "search-results.md" 221 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 222 223 See also: `SearchInstruments()`. 224 """ 225 226 self.pricesFile = "prices.md" 227 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 228 229 See also: `GetListOfPrices()`. 230 """ 231 232 self.infoFile = "info.md" 233 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 234 235 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 236 """ 237 238 self.bondsXLSXFile = "ext-bonds.xlsx" 239 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 240 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 241 242 See also: `ExtendBondsData()`. 243 """ 244 245 self.calendarFile = "calendar.md" 246 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 247 248 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 249 250 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 251 """ 252 253 self.overviewFile = "overview.md" 254 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 255 256 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 257 """ 258 259 self.overviewDigestFile = "overview-digest.md" 260 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 261 262 See also: `Overview()` with parameter `details="digest"`. 263 """ 264 265 self.overviewPositionsFile = "overview-positions.md" 266 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 267 268 See also: `Overview()` with parameter `details="positions"`. 269 """ 270 271 self.overviewOrdersFile = "overview-orders.md" 272 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 273 274 See also: `Overview()` with parameter `details="orders"`. 275 """ 276 277 self.overviewAnalyticsFile = "overview-analytics.md" 278 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 279 280 See also: `Overview()` with parameter `details="analytics"`. 281 """ 282 283 self.overviewBondsCalendarFile = "overview-calendar.md" 284 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 285 286 See also: `Overview()` with parameter `details="calendar"`. 287 """ 288 289 self.reportFile = "deals.md" 290 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 291 292 See also: `Deals()`. 293 """ 294 295 self.withdrawalLimitsFile = "limits.md" 296 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 297 298 See also: `OverviewLimits()` and `RequestLimits()`. 299 """ 300 301 self.userInfoFile = "user-info.md" 302 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 303 304 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 305 """ 306 307 self.userAccountsFile = "accounts.md" 308 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 309 310 See also: `OverviewAccounts()`, `RequestAccounts()`. 311 """ 312 313 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 314 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 315 316 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 317 318 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 319 """ 320 321 self.iList = None # init iList for raw instruments data 322 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 323 324 See also: `Listing()`, `DumpInstruments()`. 325 """ 326 327 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 328 if useCache: 329 if os.path.exists(self.iListDumpFile): 330 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 331 curTime = datetime.now(tzutc()) 332 333 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 334 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 335 336 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 337 338 else: 339 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 340 341 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 342 os.path.abspath(self.iListDumpFile), 343 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 344 )) 345 346 else: 347 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 348 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 349 350 else: 351 self.iList = self.Listing() # request new raw instruments data from broker server 352 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 353 354 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 355 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 356 357 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 358 """ 359 360 @property 361 def ticker(self) -> str: 362 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 363 364 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 365 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 366 367 See also: `SearchByTicker()`, `SearchInstruments()`. 368 """ 369 return self._ticker 370 371 @ticker.setter 372 def ticker(self, value): 373 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 374 375 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 376 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 377 378 See also: `SearchByTicker()`, `SearchInstruments()`. 379 """ 380 self._ticker = str(value).upper() # Tickers may be upper case only 381 382 @property 383 def figi(self) -> str: 384 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 385 386 See also: `SearchByFIGI()`, `SearchInstruments()`. 387 """ 388 return self._figi 389 390 @figi.setter 391 def figi(self, value): 392 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 393 394 See also: `SearchByFIGI()`, `SearchInstruments()`. 395 """ 396 self._figi = str(value).upper() # FIGI may be upper case only 397 398 def _ParseJSON(self, rawData="{}") -> dict: 399 """ 400 Parse JSON from response string. 401 402 :param rawData: this is a string with JSON-formatted text. 403 :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`. 404 """ 405 try: 406 responseJSON = json.loads(rawData) if rawData else {} 407 408 if self.moreDebug: 409 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 410 411 return responseJSON 412 413 except Exception as e: 414 uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e)) 415 return {} 416 417 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 418 """ 419 Send GET or POST request to broker server and receive JSON object. 420 421 self.header: must be defining with dictionary of headers. 422 self.body: if define then used as request body. None by default. 423 self.timeout: global request timeout, 15 seconds by default. 424 :param url: url with REST request. 425 :param reqType: send "GET" or "POST" request. "GET" by default. 426 :param retry: how many times retry after first request if an 5xx server errors occurred. 427 :param pause: sleep time in seconds between retries. 428 :return: response JSON (dictionary) from broker. 429 """ 430 if reqType.upper() not in ("GET", "POST"): 431 uLogger.error("You can define request type: `GET` or `POST`!") 432 raise Exception("Incorrect value") 433 434 if self.moreDebug: 435 uLogger.debug("Request parameters:") 436 uLogger.debug(" - REST API URL: {}".format(url)) 437 uLogger.debug(" - request type: {}".format(reqType)) 438 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 439 uLogger.debug(" - body:\n{}".format(self.body)) 440 441 # fast hack to avoid all operations with some tickers/FIGI 442 responseJSON = {} 443 oK = True 444 for item in self.exclude: 445 if item in url: 446 if self.moreDebug: 447 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 448 449 oK = False 450 break 451 452 if oK: 453 with self.__lock: # acquire the mutex lock 454 counter = 0 455 response = None 456 errMsg = "" 457 458 while not response and counter <= retry: 459 if reqType == "GET": 460 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 461 462 if reqType == "POST": 463 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 464 465 if self.moreDebug: 466 uLogger.debug("Response:") 467 uLogger.debug(" - status code: {}".format(response.status_code)) 468 uLogger.debug(" - reason: {}".format(response.reason)) 469 uLogger.debug(" - body length: {}".format(len(response.text))) 470 uLogger.debug(" - headers:\n{}".format(response.headers)) 471 472 # Server returns some headers: 473 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 474 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 475 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 476 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 477 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 478 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 479 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 480 sleep(rateLimitWait) 481 482 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 483 if 400 <= response.status_code < 500: 484 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 485 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 486 487 if "code" in response.text and "message" in response.text: 488 msgDict = self._ParseJSON(rawData=response.text) 489 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 490 491 counter = retry + 1 # do not retry for 4xx errors 492 493 if 500 <= response.status_code < 600: 494 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 495 uLogger.debug(" - not oK, {}".format(errMsg)) 496 497 if "code" in response.text and "message" in response.text: 498 errMsgDict = self._ParseJSON(rawData=response.text) 499 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 500 501 counter += 1 502 503 if counter <= retry: 504 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 505 sleep(pause) 506 507 responseJSON = self._ParseJSON(rawData=response.text) 508 509 if errMsg: 510 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 511 uLogger.error(" - not oK, {}".format(errMsg)) 512 513 return responseJSON 514 515 def _IUpdater(self, iType: str) -> tuple: 516 """ 517 Request instrument by type from server. See available API methods for instruments: 518 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 519 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 520 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 521 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 522 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 523 524 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 525 :return: tuple with iType name and list of available instruments of current type for defined user token. 526 """ 527 result = [] 528 529 if iType in TKS_INSTRUMENTS: 530 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 531 532 # all instruments have the same body in API v2 requests: 533 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 534 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 535 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 536 537 return iType, result 538 539 def _IWrapper(self, kwargs): 540 """ 541 Wrapper runs instrument's update method `_IUpdater()`. 542 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 543 """ 544 return self._IUpdater(**kwargs) 545 546 def Listing(self) -> dict: 547 """ 548 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 549 550 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 551 """ 552 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 553 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 554 555 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 556 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 557 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 558 559 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 560 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 561 poolUpdater.close() # close the thread pool 562 poolUpdater.join() # wait a moment until all data returns from threads 563 564 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 565 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 566 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 567 568 # calculate minimum price increment (step) for all instruments and set up instrument's type: 569 for iType in iList.keys(): 570 for ticker in iList[iType]: 571 iList[iType][ticker]["type"] = iType 572 573 if "minPriceIncrement" in iList[iType][ticker].keys(): 574 iList[iType][ticker]["step"] = NanoToFloat( 575 iList[iType][ticker]["minPriceIncrement"]["units"], 576 iList[iType][ticker]["minPriceIncrement"]["nano"], 577 ) 578 579 else: 580 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 581 582 return iList 583 584 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 585 """ 586 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 587 588 See also: `DumpInstruments()`, `Listing()`. 589 590 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 591 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 592 """ 593 if self.iListDumpFile is None or not self.iListDumpFile: 594 uLogger.error("Output name of dump file must be defined!") 595 raise Exception("Filename required") 596 597 if not self.iList or forceUpdate: 598 self.iList = self.Listing() 599 600 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 601 602 # Save as XLSX with separated sheets for every type of instruments: 603 with pd.ExcelWriter( 604 path=xlsxDumpFile, 605 date_format=TKS_DATE_FORMAT, 606 datetime_format=TKS_DATE_TIME_FORMAT, 607 mode="w", 608 ) as writer: 609 for iType in TKS_INSTRUMENTS: 610 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 611 df = df[sorted(df)] # sorted by column names 612 df = df.applymap( 613 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 614 na_action="ignore", 615 ) # converting numbers from nano-type to float in every cell 616 df.to_excel( 617 writer, 618 sheet_name=iType, 619 encoding="UTF-8", 620 freeze_panes=(1, 1), 621 ) # saving as XLSX-file with freeze first row and column as headers 622 623 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 624 625 def DumpInstruments(self, forceUpdate: bool = True) -> str: 626 """ 627 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 628 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 629 630 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 631 632 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 633 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 634 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 635 """ 636 if self.iListDumpFile is None or not self.iListDumpFile: 637 uLogger.error("Output name of dump file must be defined!") 638 raise Exception("Filename required") 639 640 if not self.iList or forceUpdate: 641 self.iList = self.Listing() 642 643 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 644 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 645 fH.write(jsonDump) 646 647 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 648 649 return jsonDump 650 651 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 652 """ 653 Show information about one instrument defined by json data and prints it in Markdown format. 654 655 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 656 657 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 658 :param show: if `True` then also printing information about instrument and its current price. 659 :return: multilines text in Markdown format with information about one instrument. 660 """ 661 splitLine = "| | |\n" 662 infoText = "" 663 664 if iJSON is not None and iJSON and isinstance(iJSON, dict): 665 info = [ 666 "# Main information\n\n", 667 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 668 "| Parameters | Values |\n", 669 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 670 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 671 "| Full name: | {:<54} |\n".format(iJSON["name"]), 672 ] 673 674 if "sector" in iJSON.keys() and iJSON["sector"]: 675 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 676 677 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 678 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 679 680 info.extend([ 681 splitLine, 682 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 683 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 684 ]) 685 686 if "isin" in iJSON.keys() and iJSON["isin"]: 687 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 688 689 if "classCode" in iJSON.keys(): 690 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 691 692 info.extend([ 693 splitLine, 694 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 695 splitLine, 696 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 697 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 698 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 699 ]) 700 701 if iJSON["figi"]: 702 self._figi = iJSON["figi"] 703 iJSON = iJSON | self.RequestTradingStatus() 704 705 info.extend([ 706 splitLine, 707 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 708 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 709 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 710 ]) 711 712 info.append(splitLine) 713 714 if "type" in iJSON.keys() and iJSON["type"]: 715 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 716 717 if "shareType" in iJSON.keys() and iJSON["shareType"]: 718 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 719 720 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 721 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 722 723 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 724 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 725 726 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 727 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 728 729 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 730 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 731 732 if "focusType" in iJSON.keys() and iJSON["focusType"]: 733 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 734 735 if "assetType" in iJSON.keys() and iJSON["assetType"]: 736 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 737 738 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 739 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 740 741 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 742 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 743 744 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 745 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 746 747 if "currency" in iJSON.keys(): 748 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 749 750 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 751 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 752 753 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 754 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 755 756 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 757 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 758 759 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 760 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 761 762 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 763 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 764 765 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 766 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 767 768 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 769 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 770 771 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 772 info.append("| Perpetual bond: | Yes |\n") 773 774 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 775 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 776 777 iExt = None 778 if iJSON["type"] == "Bonds": 779 info.extend([ 780 splitLine, 781 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 782 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 783 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 784 iJSON["nominal"]["currency"], 785 )), 786 ]) 787 788 if "floatingCouponFlag" in iJSON.keys(): 789 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 790 791 if "amortizationFlag" in iJSON.keys(): 792 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 793 794 info.append(splitLine) 795 796 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 797 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 798 799 if iJSON["figi"]: 800 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 801 802 info.extend([ 803 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 804 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 805 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 806 ]) 807 808 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 809 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 810 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 811 iJSON["aciValue"]["currency"] 812 ))) 813 814 if "currentPrice" in iJSON.keys(): 815 info.append(splitLine) 816 817 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 818 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 819 820 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 821 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 822 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 823 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 824 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 825 826 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 827 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 828 829 info.extend([ 830 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 831 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 832 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 833 )), 834 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 835 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 836 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 837 )), 838 "| Changes between last deal price and last close | {:<54} |\n".format( 839 "{:.2f}%{}".format( 840 iJSON["currentPrice"]["changes"], 841 " ({}{:.2f} {})".format( 842 "+" if bondChangesDelta > 0 else "", 843 bondChangesDelta, 844 aciCurrency 845 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 846 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 847 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 848 currency 849 ), 850 ) 851 ), 852 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 853 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 854 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 855 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 856 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 857 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 858 )), 859 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 860 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 861 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 862 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 863 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 864 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 865 )), 866 ]) 867 868 if "lot" in iJSON.keys(): 869 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 870 871 if "step" in iJSON.keys() and iJSON["step"] != 0: 872 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 873 874 # Add bond payment calendar: 875 if iJSON["type"] == "Bonds": 876 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 877 info.extend(["\n#", strCalendar]) 878 879 infoText += "".join(info) 880 881 if show: 882 uLogger.info("{}".format(infoText)) 883 884 else: 885 uLogger.debug("{}".format(infoText)) 886 887 if self.infoFile is not None: 888 with open(self.infoFile, "w", encoding="UTF-8") as fH: 889 fH.write(infoText) 890 891 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 892 893 if self.useHTMLReports: 894 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 895 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 896 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 897 898 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 899 900 return infoText 901 902 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 903 """ 904 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 905 906 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 907 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 908 :return: JSON formatted data with information about instrument. 909 """ 910 tickerJSON = {} 911 if self.moreDebug: 912 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 913 914 if not self._ticker: 915 uLogger.warning("self._ticker variable is not be empty!") 916 917 else: 918 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 919 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 920 raise Exception("Instrument not allowed") 921 922 if not self.iList: 923 self.iList = self.Listing() 924 925 if self._ticker in self.iList["Shares"].keys(): 926 tickerJSON = self.iList["Shares"][self._ticker] 927 if self.moreDebug: 928 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 929 930 elif self._ticker in self.iList["Currencies"].keys(): 931 tickerJSON = self.iList["Currencies"][self._ticker] 932 if self.moreDebug: 933 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 934 935 elif self._ticker in self.iList["Bonds"].keys(): 936 tickerJSON = self.iList["Bonds"][self._ticker] 937 if self.moreDebug: 938 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 939 940 elif self._ticker in self.iList["Etfs"].keys(): 941 tickerJSON = self.iList["Etfs"][self._ticker] 942 if self.moreDebug: 943 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 944 945 elif self._ticker in self.iList["Futures"].keys(): 946 tickerJSON = self.iList["Futures"][self._ticker] 947 if self.moreDebug: 948 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 949 950 if tickerJSON: 951 self._figi = tickerJSON["figi"] 952 953 if requestPrice: 954 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 955 956 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 957 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 958 959 else: 960 tickerJSON["currentPrice"]["changes"] = 0 961 962 if show: 963 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 964 965 else: 966 if show: 967 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 968 969 return tickerJSON 970 971 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 972 """ 973 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 974 975 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 976 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 977 :return: JSON formatted data with information about instrument. 978 """ 979 figiJSON = {} 980 if self.moreDebug: 981 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 982 983 if not self._figi: 984 uLogger.warning("self._figi variable is not be empty!") 985 986 else: 987 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 988 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 989 raise Exception("Instrument not allowed") 990 991 if not self.iList: 992 self.iList = self.Listing() 993 994 for item in self.iList["Shares"].keys(): 995 if self._figi == self.iList["Shares"][item]["figi"]: 996 figiJSON = self.iList["Shares"][item] 997 998 if self.moreDebug: 999 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1000 1001 break 1002 1003 if not figiJSON: 1004 for item in self.iList["Currencies"].keys(): 1005 if self._figi == self.iList["Currencies"][item]["figi"]: 1006 figiJSON = self.iList["Currencies"][item] 1007 1008 if self.moreDebug: 1009 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1010 1011 break 1012 1013 if not figiJSON: 1014 for item in self.iList["Bonds"].keys(): 1015 if self._figi == self.iList["Bonds"][item]["figi"]: 1016 figiJSON = self.iList["Bonds"][item] 1017 1018 if self.moreDebug: 1019 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1020 1021 break 1022 1023 if not figiJSON: 1024 for item in self.iList["Etfs"].keys(): 1025 if self._figi == self.iList["Etfs"][item]["figi"]: 1026 figiJSON = self.iList["Etfs"][item] 1027 1028 if self.moreDebug: 1029 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1030 1031 break 1032 1033 if not figiJSON: 1034 for item in self.iList["Futures"].keys(): 1035 if self._figi == self.iList["Futures"][item]["figi"]: 1036 figiJSON = self.iList["Futures"][item] 1037 1038 if self.moreDebug: 1039 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1040 1041 break 1042 1043 if figiJSON: 1044 self._figi = figiJSON["figi"] 1045 self._ticker = figiJSON["ticker"] 1046 1047 if requestPrice: 1048 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1049 1050 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1051 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1052 1053 else: 1054 figiJSON["currentPrice"]["changes"] = 0 1055 1056 if show: 1057 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1058 1059 else: 1060 if show: 1061 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1062 1063 return figiJSON 1064 1065 def GetCurrentPrices(self, show: bool = True) -> dict: 1066 """ 1067 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1068 `{"buy": [{"price": 1243.8, "quantity": 193}, 1069 {"price": 1244.0, "quantity": 168}, 1070 {"price": 1244.8, "quantity": 5}, 1071 {"price": 1245.0, "quantity": 61}, 1072 {"price": 1245.4, "quantity": 60}], 1073 "sell": [{"price": 1243.6, "quantity": 8}, 1074 {"price": 1242.6, "quantity": 10}, 1075 {"price": 1242.4, "quantity": 18}, 1076 {"price": 1242.2, "quantity": 50}, 1077 {"price": 1242.0, "quantity": 113}], 1078 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1079 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1080 - sell: list of dicts with Buyers prices, 1081 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1082 - quantity: volume value by current price in lots, 1083 - limitUp: current trade session limit price, maximum, 1084 - limitDown: current trade session limit price, minimum, 1085 - lastPrice: last deal price of the instrument, 1086 - closePrice: previous trade session close price of the instrument. 1087 1088 See also: `SearchByTicker()` and `SearchByFIGI()`. 1089 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1090 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1091 1092 :param show: if `True` then print DOM to log and console. 1093 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1094 If an error occurred then returns an empty record: 1095 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1096 """ 1097 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1098 1099 if self.depth < 1: 1100 uLogger.error("Depth of Market (DOM) must be >=1!") 1101 raise Exception("Incorrect value") 1102 1103 if not (self._ticker or self._figi): 1104 uLogger.error("self._ticker or self._figi variables must be defined!") 1105 raise Exception("Ticker or FIGI required") 1106 1107 if self._ticker and not self._figi: 1108 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1109 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1110 1111 if not self._ticker and self._figi: 1112 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1113 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1114 1115 if not self._figi: 1116 uLogger.error("FIGI is not defined!") 1117 raise Exception("Ticker or FIGI required") 1118 1119 else: 1120 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1121 1122 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1123 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1124 self.body = str({"figi": self._figi, "depth": self.depth}) 1125 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1126 1127 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1128 # list of dicts with sellers orders: 1129 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1130 1131 # list of dicts with buyers orders: 1132 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1133 1134 # max price of instrument at this time: 1135 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1136 1137 # min price of instrument at this time: 1138 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1139 1140 # last price of deal with instrument: 1141 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1142 1143 # last close price of instrument: 1144 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1145 1146 else: 1147 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1148 uLogger.debug("Server response: {}".format(pricesResponse)) 1149 1150 if show: 1151 if prices["buy"] or prices["sell"]: 1152 info = [ 1153 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1154 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1155 self._ticker, 1156 self._figi, 1157 self.depth, 1158 ), 1159 "-" * 60, "\n", 1160 " Orders of Buyers | Orders of Sellers\n", 1161 "-" * 60, "\n", 1162 " Sell prices (volumes) | Buy prices (volumes)\n", 1163 "-" * 60, "\n", 1164 ] 1165 1166 if not prices["buy"]: 1167 info.append(" | No orders!\n") 1168 sumBuy = 0 1169 1170 else: 1171 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1172 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1173 for item in maxMinSorted: 1174 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1175 1176 if not prices["sell"]: 1177 info.append("No orders! |\n") 1178 sumSell = 0 1179 1180 else: 1181 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1182 for item in prices["sell"]: 1183 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1184 1185 info.extend([ 1186 "-" * 60, "\n", 1187 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1188 "-" * 60, "\n", 1189 ]) 1190 1191 infoText = "".join(info) 1192 1193 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1194 1195 else: 1196 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1197 1198 return prices 1199 1200 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1201 """ 1202 This method get and show information about all available broker instruments for current user account. 1203 If `instrumentsFile` string is not empty then also save information to this file. 1204 1205 :param show: if `True` then print results to console, if `False` — print only to file. 1206 :return: multi-lines string with all available broker instruments 1207 """ 1208 if not self.iList: 1209 self.iList = self.Listing() 1210 1211 info = [ 1212 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1213 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1214 ] 1215 1216 # add instruments count by type: 1217 for iType in self.iList.keys(): 1218 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1219 1220 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1221 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1222 1223 # generating info tables with all instruments by type: 1224 for iType in self.iList.keys(): 1225 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1226 1227 for instrument in self.iList[iType].keys(): 1228 iName = self.iList[iType][instrument]["name"] # instrument's name 1229 if len(iName) > 57: 1230 iName = "{}...".format(iName[:54]) # right trim for a long string 1231 1232 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1233 self.iList[iType][instrument]["ticker"], 1234 iName, 1235 self.iList[iType][instrument]["figi"], 1236 self.iList[iType][instrument]["currency"], 1237 self.iList[iType][instrument]["lot"], 1238 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1239 )) 1240 1241 infoText = "".join(info) 1242 1243 if show: 1244 uLogger.info(infoText) 1245 1246 if self.instrumentsFile: 1247 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1248 fH.write(infoText) 1249 1250 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1251 1252 if self.useHTMLReports: 1253 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1254 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1255 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1256 1257 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1258 1259 return infoText 1260 1261 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1262 """ 1263 This method search and show information about instruments by part of its ticker, FIGI or name. 1264 If `searchResultsFile` string is not empty then also save information to this file. 1265 1266 :param pattern: string with part of ticker, FIGI or instrument's name. 1267 :param show: if `True` then print results to console, if `False` — return list of result only. 1268 :return: list of dictionaries with all found instruments. 1269 """ 1270 if not self.iList: 1271 self.iList = self.Listing() 1272 1273 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1274 compiledPattern = re.compile(pattern, re.IGNORECASE) 1275 1276 for iType in self.iList: 1277 for instrument in self.iList[iType].values(): 1278 searchResult = compiledPattern.search(" ".join( 1279 [instrument["ticker"], instrument["figi"], instrument["name"]] 1280 )) 1281 1282 if searchResult: 1283 searchResults[iType][instrument["ticker"]] = instrument 1284 1285 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1286 info = [ 1287 "# Search results\n\n", 1288 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1289 "* **Search pattern:** [{}]\n".format(pattern), 1290 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1291 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1292 ] 1293 infoShort = info[:] 1294 1295 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1296 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1297 skippedLine = "| ... | ... | ... | ... |\n" 1298 1299 if resultsLen == 0: 1300 info.append("\nNo results\n") 1301 infoShort.append("\nNo results\n") 1302 uLogger.warning("No results. Try changing your search pattern.") 1303 1304 else: 1305 for iType in searchResults: 1306 iTypeValuesCount = len(searchResults[iType].values()) 1307 if iTypeValuesCount > 0: 1308 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1309 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1310 1311 for instrument in searchResults[iType].values(): 1312 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1313 instrument["type"], 1314 instrument["ticker"], 1315 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1316 instrument["figi"], 1317 )) 1318 1319 if iTypeValuesCount <= 5: 1320 infoShort.extend(info[-iTypeValuesCount:]) 1321 1322 else: 1323 infoShort.extend(info[-5:]) 1324 infoShort.append(skippedLine) 1325 1326 infoText = "".join(info) 1327 infoTextShort = "".join(infoShort) 1328 1329 if show: 1330 uLogger.info(infoTextShort) 1331 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1332 1333 if self.searchResultsFile: 1334 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1335 fH.write(infoText) 1336 1337 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1338 1339 if self.useHTMLReports: 1340 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1341 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1342 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1343 1344 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1345 1346 return searchResults 1347 1348 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1349 """ 1350 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1351 1352 :param instruments: list of strings with tickers or FIGIs. 1353 :return: list with unique instrument FIGIs only. 1354 """ 1355 requestedInstruments = [] 1356 for iName in instruments: 1357 if iName not in self.aliases.keys(): 1358 if iName not in requestedInstruments: 1359 requestedInstruments.append(iName) 1360 1361 else: 1362 if iName not in requestedInstruments: 1363 if self.aliases[iName] not in requestedInstruments: 1364 requestedInstruments.append(self.aliases[iName]) 1365 1366 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1367 1368 onlyUniqueFIGIs = [] 1369 for iName in requestedInstruments: 1370 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1371 continue 1372 1373 self._ticker = iName 1374 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1375 1376 if not iData: 1377 self._ticker = "" 1378 self._figi = iName 1379 1380 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1381 1382 if not iData: 1383 self._figi = "" 1384 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1385 1386 if iData and iData["figi"] not in onlyUniqueFIGIs: 1387 onlyUniqueFIGIs.append(iData["figi"]) 1388 1389 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1390 1391 return onlyUniqueFIGIs 1392 1393 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1394 """ 1395 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1396 1397 See limits: https://tinkoff.github.io/investAPI/limits/ 1398 1399 If `pricesFile` string is not empty then also save information to this file. 1400 1401 :param instruments: list of strings with tickers or FIGIs. 1402 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1403 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1404 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1405 """ 1406 if instruments is None or not instruments: 1407 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1408 raise Exception("Ticker or FIGI required") 1409 1410 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1411 1412 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1413 1414 iList = [] # trying to get info and current prices about all unique instruments: 1415 for self._figi in onlyUniqueFIGIs: 1416 iData = self.SearchByFIGI(requestPrice=True) 1417 iList.append(iData) 1418 1419 self.ShowListOfPrices(iList, show) 1420 1421 return iList 1422 1423 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1424 """ 1425 Show table contains current prices of given instruments. 1426 1427 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1428 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1429 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1430 :return: multilines text in Markdown format as a table contains current prices. 1431 """ 1432 infoText = "" 1433 1434 if show or self.pricesFile: 1435 info = [ 1436 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1437 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1438 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1439 ] 1440 1441 for item in iList: 1442 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1443 item["ticker"], 1444 item["figi"], 1445 item["type"], 1446 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1447 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1448 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1449 "{} / {}".format( 1450 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1451 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1452 ), 1453 "{} / {}".format( 1454 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1455 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1456 ), 1457 item["currency"], 1458 )) 1459 1460 infoText = "".join(info) 1461 1462 if show: 1463 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1464 1465 if self.pricesFile: 1466 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1467 fH.write(infoText) 1468 1469 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1470 1471 if self.useHTMLReports: 1472 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1473 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1474 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1475 1476 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1477 1478 return infoText 1479 1480 def RequestTradingStatus(self) -> dict: 1481 """ 1482 Requesting trading status for the instrument defined by `figi` variable. 1483 1484 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1485 1486 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1487 1488 :return: dictionary with trading status attributes. Response example: 1489 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1490 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1491 """ 1492 if self._figi is None or not self._figi: 1493 uLogger.error("Variable `figi` must be defined for using this method!") 1494 raise Exception("FIGI required") 1495 1496 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1497 1498 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1499 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1500 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1501 1502 if self.moreDebug: 1503 uLogger.debug("Records about current trading status successfully received") 1504 1505 return tradingStatus 1506 1507 def RequestPortfolio(self) -> dict: 1508 """ 1509 Requesting actual user's portfolio for current `accountId`. 1510 1511 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1512 1513 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1514 1515 :return: dictionary with user's portfolio. 1516 """ 1517 if self.accountId is None or not self.accountId: 1518 uLogger.error("Variable `accountId` must be defined for using this method!") 1519 raise Exception("Account ID required") 1520 1521 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1522 1523 self.body = str({"accountId": self.accountId}) 1524 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1525 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1526 1527 if self.moreDebug: 1528 uLogger.debug("Records about user's portfolio successfully received") 1529 1530 return rawPortfolio 1531 1532 def RequestPositions(self) -> dict: 1533 """ 1534 Requesting open positions by currencies and instruments for current `accountId`. 1535 1536 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1537 1538 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1539 1540 :return: dictionary with open positions by instruments. 1541 """ 1542 if self.accountId is None or not self.accountId: 1543 uLogger.error("Variable `accountId` must be defined for using this method!") 1544 raise Exception("Account ID required") 1545 1546 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1547 1548 self.body = str({"accountId": self.accountId}) 1549 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1550 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1551 1552 if self.moreDebug: 1553 uLogger.debug("Records about current open positions successfully received") 1554 1555 return rawPositions 1556 1557 def RequestPendingOrders(self) -> list: 1558 """ 1559 Requesting current actual pending limit orders for current `accountId`. 1560 1561 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1562 1563 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1564 1565 :return: list of dictionaries with pending limit orders. 1566 """ 1567 if self.accountId is None or not self.accountId: 1568 uLogger.error("Variable `accountId` must be defined for using this method!") 1569 raise Exception("Account ID required") 1570 1571 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1572 1573 self.body = str({"accountId": self.accountId}) 1574 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1575 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1576 1577 if "orders" in rawResponse.keys(): 1578 rawOrders = rawResponse["orders"] 1579 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1580 1581 else: 1582 rawOrders = [] 1583 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1584 1585 return rawOrders 1586 1587 def RequestStopOrders(self) -> list: 1588 """ 1589 Requesting current actual stop orders for current `accountId`. 1590 1591 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1592 1593 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1594 1595 :return: list of dictionaries with stop orders. 1596 """ 1597 if self.accountId is None or not self.accountId: 1598 uLogger.error("Variable `accountId` must be defined for using this method!") 1599 raise Exception("Account ID required") 1600 1601 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1602 1603 self.body = str({"accountId": self.accountId}) 1604 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1605 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1606 1607 if "stopOrders" in rawResponse.keys(): 1608 rawStopOrders = rawResponse["stopOrders"] 1609 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1610 1611 else: 1612 rawStopOrders = [] 1613 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1614 1615 return rawStopOrders 1616 1617 def Overview(self, show: bool = False, details: str = "full") -> dict: 1618 """ 1619 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1620 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1621 and `overviewBondsCalendarFile` are defined then also save information to file. 1622 1623 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1624 many requests about the state of the portfolio, and then, based on the received data, a large number 1625 of calculation and statistics are collected. 1626 1627 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1628 :param details: how detailed should the information be? 1629 - `full` — shows full available information about portfolio status (by default), 1630 - `positions` — shows only open positions, 1631 - `orders` — shows only sections of open limits and stop orders. 1632 - `digest` — show a short digest of the portfolio status, 1633 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1634 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1635 :return: dictionary with client's raw portfolio and some statistics. 1636 """ 1637 if self.accountId is None or not self.accountId: 1638 uLogger.error("Variable `accountId` must be defined for using this method!") 1639 raise Exception("Account ID required") 1640 1641 view = { 1642 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1643 "headers": {}, # list of dictionaries, response headers without "positions" section 1644 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1645 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1646 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1647 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1648 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1649 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1650 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1651 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1652 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1653 }, 1654 "stat": { # --- some statistics calculated using "raw" sections: 1655 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1656 "availableRUB": 0., # available rubles (without other currencies) 1657 "blockedRUB": 0., # blocked sum in Russian Rouble 1658 "totalChangesRUB": 0., # changes for all open trades in RUB 1659 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1660 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1661 "sharesCostRUB": 0., # costs of all shares in RUB 1662 "bondsCostRUB": 0., # costs of all bonds in RUB 1663 "etfsCostRUB": 0., # costs of all etfs in RUB 1664 "futuresCostRUB": 0., # costs of all futures in RUB 1665 "Currencies": [], # list of dictionaries of all currencies statistics 1666 "Shares": [], # list of dictionaries of all shares statistics 1667 "Bonds": [], # list of dictionaries of all bonds statistics 1668 "Etfs": [], # list of dictionaries of all etfs statistics 1669 "Futures": [], # list of dictionaries of all futures statistics 1670 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1671 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1672 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1673 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1674 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1675 }, 1676 "analytics": { # --- some analytics of portfolio: 1677 "distrByAssets": {}, # portfolio distribution by assets 1678 "distrByCompanies": {}, # portfolio distribution by companies 1679 "distrBySectors": {}, # portfolio distribution by sectors 1680 "distrByCurrencies": {}, # portfolio distribution by currencies 1681 "distrByCountries": {}, # portfolio distribution by countries 1682 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1683 } 1684 } 1685 1686 details = details.lower() 1687 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1688 if details not in availableDetails: 1689 details = "full" 1690 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1691 1692 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1693 1694 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1695 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1696 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1697 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1698 1699 # save response headers without "positions" section: 1700 for key in portfolioResponse.keys(): 1701 if key != "positions": 1702 view["raw"]["headers"][key] = portfolioResponse[key] 1703 1704 else: 1705 continue 1706 1707 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1708 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1709 for item in portfolioResponse["positions"]: 1710 if item["instrumentType"] == "currency": 1711 self._figi = item["figi"] 1712 if not self._figi and item["ticker"]: 1713 self._ticker = item["ticker"] 1714 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1715 1716 curr = self.SearchByFIGI(requestPrice=False) 1717 1718 # current price of currency in RUB: 1719 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1720 "name": curr["name"], 1721 "currentPrice": NanoToFloat( 1722 item["currentPrice"]["units"], 1723 item["currentPrice"]["nano"] 1724 ), 1725 } 1726 1727 view["raw"]["Currencies"].append(item) 1728 1729 elif item["instrumentType"] == "share": 1730 view["raw"]["Shares"].append(item) 1731 1732 elif item["instrumentType"] == "bond": 1733 view["raw"]["Bonds"].append(item) 1734 1735 elif item["instrumentType"] == "etf": 1736 view["raw"]["Etfs"].append(item) 1737 1738 elif item["instrumentType"] == "futures": 1739 view["raw"]["Futures"].append(item) 1740 1741 else: 1742 continue 1743 1744 # how many volume of currencies (by ISO currency name) are blocked: 1745 for item in view["raw"]["positions"]["blocked"]: 1746 blocked = NanoToFloat(item["units"], item["nano"]) 1747 if blocked > 0: 1748 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1749 1750 # how many volume of instruments (by FIGI) are blocked: 1751 for item in view["raw"]["positions"]["securities"]: 1752 blocked = int(item["blocked"]) 1753 if blocked > 0: 1754 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1755 1756 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1757 1758 if "rub" in allBlocked.keys(): 1759 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1760 1761 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1762 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1763 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1764 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1765 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1766 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1767 view["stat"]["portfolioCostRUB"] = sum([ 1768 view["stat"]["allCurrenciesCostRUB"], 1769 view["stat"]["sharesCostRUB"], 1770 view["stat"]["bondsCostRUB"], 1771 view["stat"]["etfsCostRUB"], 1772 view["stat"]["futuresCostRUB"], 1773 ]) 1774 1775 # --- calculating some portfolio statistics: 1776 byComp = {} # distribution by companies 1777 bySect = {} # distribution by sectors 1778 byCurr = {} # distribution by currencies (include RUB) 1779 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1780 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1781 1782 for item in portfolioResponse["positions"]: 1783 self._figi = item["figi"] 1784 if not self._figi and item["ticker"]: 1785 self._ticker = item["ticker"] 1786 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1787 1788 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1789 1790 if instrument: 1791 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1792 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1793 1794 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1795 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1796 1797 else: 1798 blocked = 0 1799 1800 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1801 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1802 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1803 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1804 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1805 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1806 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1807 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1808 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1809 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1810 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1811 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1812 1813 statData = { 1814 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1815 "ticker": instrument["ticker"], # ticker by FIGI 1816 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1817 "volume": volume, # available volume of instrument 1818 "lots": lots, # volume in lots of instrument 1819 "direction": direction, # direction of an instrument's position: short or long 1820 "blocked": blocked, # blocked volume of currency or instrument 1821 "currentPrice": curPrice, # current instrument's price in basic asset 1822 "average": average, # current average position price 1823 "cost": cost, # current cost of all volume of instrument in basic asset 1824 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1825 "costRUB": costRUB, # cost of instrument in ruble 1826 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1827 "profit": profit, # expected profit at current moment 1828 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1829 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1830 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1831 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1832 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1833 "step": instrument["step"], # minimum price increment 1834 } 1835 1836 # adding distribution by unique countries: 1837 if statData["country"] not in byCountry.keys(): 1838 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1839 1840 else: 1841 byCountry[statData["country"]]["cost"] += costRUB 1842 byCountry[statData["country"]]["percent"] += percentCostRUB 1843 1844 if item["instrumentType"] != "currency": 1845 # adding distribution by unique companies: 1846 if statData["name"]: 1847 if statData["name"] not in byComp.keys(): 1848 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1849 1850 else: 1851 byComp[statData["name"]]["cost"] += costRUB 1852 byComp[statData["name"]]["percent"] += percentCostRUB 1853 1854 # adding distribution by unique sectors: 1855 if statData["sector"] not in bySect.keys(): 1856 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1857 1858 else: 1859 bySect[statData["sector"]]["cost"] += costRUB 1860 bySect[statData["sector"]]["percent"] += percentCostRUB 1861 1862 # adding distribution by unique currencies: 1863 if currency not in byCurr.keys(): 1864 byCurr[currency] = { 1865 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1866 "cost": costRUB, 1867 "percent": percentCostRUB 1868 } 1869 1870 else: 1871 byCurr[currency]["cost"] += costRUB 1872 byCurr[currency]["percent"] += percentCostRUB 1873 1874 # saving statistics for every instrument: 1875 if item["instrumentType"] == "currency": 1876 view["stat"]["Currencies"].append(statData) 1877 1878 # update dict with free funds for trading (total - blocked) by currencies 1879 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1880 view["stat"]["funds"][currency] = { 1881 "total": volume, 1882 "totalCostRUB": costRUB, # total volume cost in rubles 1883 "free": volume - blocked, 1884 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1885 } 1886 1887 elif item["instrumentType"] == "share": 1888 view["stat"]["Shares"].append(statData) 1889 1890 elif item["instrumentType"] == "bond": 1891 view["stat"]["Bonds"].append(statData) 1892 1893 elif item["instrumentType"] == "etf": 1894 view["stat"]["Etfs"].append(statData) 1895 1896 elif item["instrumentType"] == "Futures": 1897 view["stat"]["Futures"].append(statData) 1898 1899 else: 1900 continue 1901 1902 # total changes in Russian Ruble: 1903 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1904 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1905 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1906 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1907 view["stat"]["funds"]["rub"] = { 1908 "total": view["stat"]["availableRUB"], 1909 "totalCostRUB": view["stat"]["availableRUB"], 1910 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1911 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1912 } 1913 1914 # --- pending limit orders sector data: 1915 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1916 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1917 1918 for item in view["raw"]["orders"]: 1919 self._figi = item["figi"] 1920 1921 if item["figi"] not in uniquePendingOrdersFIGIs: 1922 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1923 1924 uniquePendingOrdersFIGIs.append(item["figi"]) 1925 uniquePendingOrders[item["figi"]] = instrument 1926 1927 else: 1928 instrument = uniquePendingOrders[item["figi"]] 1929 1930 if instrument: 1931 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1932 orderType = TKS_ORDER_TYPES[item["orderType"]] 1933 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1934 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1935 1936 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1937 if item["direction"] == "ORDER_DIRECTION_BUY": 1938 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1939 1940 else: 1941 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1942 1943 # requested price for order execution: 1944 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1945 1946 # necessary changes in percent to reach target from current price: 1947 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1948 1949 view["stat"]["orders"].append({ 1950 "orderID": item["orderId"], # orderId number parameter of current order 1951 "figi": item["figi"], # FIGI identification 1952 "ticker": instrument["ticker"], # ticker name by FIGI 1953 "lotsRequested": item["lotsRequested"], # requested lots value 1954 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1955 "currentPrice": lastPrice, # current instrument's price for defined action 1956 "targetPrice": target, # requested price for order execution in base currency 1957 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1958 "percentChanges": changes, # changes in percent to target from current price 1959 "currency": item["currency"], # instrument's currency name 1960 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1961 "type": orderType, # type of order from TKS_ORDER_TYPES 1962 "status": orderState, # order status from TKS_ORDER_STATES 1963 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1964 }) 1965 1966 # --- stop orders sector data: 1967 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1968 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1969 1970 for item in view["raw"]["stopOrders"]: 1971 self._figi = item["figi"] 1972 1973 if item["figi"] not in uniqueStopOrdersFIGIs: 1974 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1975 1976 uniqueStopOrdersFIGIs.append(item["figi"]) 1977 uniqueStopOrders[item["figi"]] = instrument 1978 1979 else: 1980 instrument = uniqueStopOrders[item["figi"]] 1981 1982 if instrument: 1983 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1984 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1985 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1986 1987 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1988 if "expirationTime" in item.keys(): 1989 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1990 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1991 1992 else: 1993 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1994 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1995 1996 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1997 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1998 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1999 2000 else: 2001 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2002 2003 # requested price when stop-order executed: 2004 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2005 2006 # price for limit-order, set up when stop-order executed: 2007 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2008 2009 # necessary changes in percent to reach target from current price: 2010 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2011 2012 view["stat"]["stopOrders"].append({ 2013 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2014 "figi": item["figi"], # FIGI identification 2015 "ticker": instrument["ticker"], # ticker name by FIGI 2016 "lotsRequested": item["lotsRequested"], # requested lots value 2017 "currentPrice": lastPrice, # current instrument's price for defined action 2018 "targetPrice": target, # requested price for stop-order execution in base currency 2019 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2020 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2021 "percentChanges": changes, # changes in percent to target from current price 2022 "currency": item["currency"], # instrument's currency name 2023 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2024 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2025 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2026 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2027 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2028 }) 2029 2030 # --- calculating data for analytics section: 2031 # portfolio distribution by assets: 2032 view["analytics"]["distrByAssets"] = { 2033 "Ruble": { 2034 "uniques": 1, 2035 "cost": view["stat"]["availableRUB"], 2036 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2037 }, 2038 "Currencies": { 2039 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2040 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2041 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2042 }, 2043 "Shares": { 2044 "uniques": len(view["stat"]["Shares"]), 2045 "cost": view["stat"]["sharesCostRUB"], 2046 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2047 }, 2048 "Bonds": { 2049 "uniques": len(view["stat"]["Bonds"]), 2050 "cost": view["stat"]["bondsCostRUB"], 2051 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2052 }, 2053 "Etfs": { 2054 "uniques": len(view["stat"]["Etfs"]), 2055 "cost": view["stat"]["etfsCostRUB"], 2056 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2057 }, 2058 "Futures": { 2059 "uniques": len(view["stat"]["Futures"]), 2060 "cost": view["stat"]["futuresCostRUB"], 2061 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2062 }, 2063 } 2064 2065 # portfolio distribution by companies: 2066 view["analytics"]["distrByCompanies"]["All money cash"] = { 2067 "ticker": "", 2068 "cost": view["stat"]["allCurrenciesCostRUB"], 2069 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2070 } 2071 view["analytics"]["distrByCompanies"].update(byComp) 2072 2073 # portfolio distribution by sectors: 2074 view["analytics"]["distrBySectors"]["All money cash"] = { 2075 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2076 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2077 } 2078 view["analytics"]["distrBySectors"].update(bySect) 2079 2080 # portfolio distribution by currencies: 2081 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2082 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2083 2084 if self.moreDebug: 2085 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2086 2087 view["analytics"]["distrByCurrencies"].update(byCurr) 2088 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2089 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2090 2091 # portfolio distribution by countries: 2092 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2093 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2094 2095 if self.moreDebug: 2096 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2097 2098 view["analytics"]["distrByCountries"].update(byCountry) 2099 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2100 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2101 2102 # --- Prepare text statistics overview in human-readable: 2103 if show: 2104 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2105 2106 # Whatever the value `details`, header not changes: 2107 info = [ 2108 "# Client's portfolio\n\n", 2109 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2110 "* **Account ID:** [{}]\n".format(self.accountId), 2111 ] 2112 2113 if details in ["full", "positions", "digest"]: 2114 info.extend([ 2115 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2116 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2117 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2118 view["stat"]["totalChangesRUB"], 2119 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2120 view["stat"]["totalChangesPercentRUB"], 2121 ), 2122 ]) 2123 2124 if details in ["full", "positions"]: 2125 info.extend([ 2126 "## Open positions\n\n", 2127 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2128 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2129 "| **Ruble:** | {:>31} | | | | | |\n".format( 2130 "{:.2f} ({:.2f}) rub".format( 2131 view["stat"]["availableRUB"], 2132 view["stat"]["blockedRUB"], 2133 ) 2134 ) 2135 ]) 2136 2137 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2138 return [ 2139 "| | | | | | | |\n", 2140 "| {:<27} | | | | | {:>19} | |\n".format( 2141 noTradeStr if noTradeStr else typeStr, 2142 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2143 ), 2144 ] 2145 2146 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2147 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2148 "{} [{}]".format(data["ticker"], data["figi"]), 2149 "{:.2f} ({:.2f}) {}".format( 2150 data["volume"], 2151 data["blocked"], 2152 data["currency"], 2153 ) if isCurr else "{:.0f} ({:.0f})".format( 2154 data["volume"], 2155 data["blocked"], 2156 ), 2157 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2158 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2159 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2160 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2161 "{}{:.2f} {} ({}{:.2f}%)".format( 2162 "+" if data["profit"] > 0 else "", 2163 data["profit"], data["baseCurrencyName"], 2164 "+" if data["percentProfit"] > 0 else "", 2165 data["percentProfit"], 2166 ), 2167 ) 2168 2169 # --- Show currencies section: 2170 if view["stat"]["Currencies"]: 2171 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2172 for item in view["stat"]["Currencies"]: 2173 info.append(_InfoStr(item, isCurr=True)) 2174 2175 else: 2176 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2177 2178 # --- Show shares section: 2179 if view["stat"]["Shares"]: 2180 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2181 2182 for item in view["stat"]["Shares"]: 2183 info.append(_InfoStr(item)) 2184 2185 else: 2186 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2187 2188 # --- Show bonds section: 2189 if view["stat"]["Bonds"]: 2190 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2191 2192 for item in view["stat"]["Bonds"]: 2193 info.append(_InfoStr(item)) 2194 2195 else: 2196 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2197 2198 # --- Show etfs section: 2199 if view["stat"]["Etfs"]: 2200 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2201 2202 for item in view["stat"]["Etfs"]: 2203 info.append(_InfoStr(item)) 2204 2205 else: 2206 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2207 2208 # --- Show futures section: 2209 if view["stat"]["Futures"]: 2210 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2211 2212 for item in view["stat"]["Futures"]: 2213 info.append(_InfoStr(item)) 2214 2215 else: 2216 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2217 2218 if details in ["full", "orders"]: 2219 # --- Show pending limit orders section: 2220 if view["stat"]["orders"]: 2221 info.extend([ 2222 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2223 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2224 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2225 ]) 2226 2227 for item in view["stat"]["orders"]: 2228 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2229 "{} [{}]".format(item["ticker"], item["figi"]), 2230 item["orderID"], 2231 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2232 "{} {} ({}{:.2f}%)".format( 2233 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2234 item["baseCurrencyName"], 2235 "+" if item["percentChanges"] > 0 else "", 2236 float(item["percentChanges"]), 2237 ), 2238 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2239 item["action"], 2240 item["type"], 2241 item["date"], 2242 )) 2243 2244 else: 2245 info.append("\n## Total pending limit-orders: [0]\n") 2246 2247 # --- Show stop orders section: 2248 if view["stat"]["stopOrders"]: 2249 info.extend([ 2250 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2251 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2252 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2253 ]) 2254 2255 for item in view["stat"]["stopOrders"]: 2256 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2257 "{} [{}]".format(item["ticker"], item["figi"]), 2258 item["orderID"], 2259 item["lotsRequested"], 2260 "{} {} ({}{:.2f}%)".format( 2261 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2262 item["baseCurrencyName"], 2263 "+" if item["percentChanges"] > 0 else "", 2264 float(item["percentChanges"]), 2265 ), 2266 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2267 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2268 item["action"], 2269 item["type"], 2270 item["expType"], 2271 item["createDate"], 2272 item["expDate"], 2273 )) 2274 2275 else: 2276 info.append("\n## Total stop-orders: [0]\n") 2277 2278 if details in ["full", "analytics"]: 2279 # -- Show analytics section: 2280 if view["stat"]["portfolioCostRUB"] > 0: 2281 info.extend([ 2282 "\n# Analytics\n\n" 2283 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2284 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2285 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2286 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2287 view["stat"]["totalChangesRUB"], 2288 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2289 view["stat"]["totalChangesPercentRUB"], 2290 ), 2291 "\n## Portfolio distribution by assets\n" 2292 "\n| Type | Uniques | Percent | Current cost |\n", 2293 "|------------------------------------|---------|---------|--------------------|\n", 2294 ]) 2295 2296 for key in view["analytics"]["distrByAssets"].keys(): 2297 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2298 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2299 key, 2300 view["analytics"]["distrByAssets"][key]["uniques"], 2301 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2302 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2303 )) 2304 2305 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2306 2307 info.extend([ 2308 "\n## Portfolio distribution by companies\n" 2309 "\n| Company | Percent | Current cost |\n", 2310 aSepLine, 2311 ]) 2312 2313 for company in view["analytics"]["distrByCompanies"].keys(): 2314 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2315 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2316 "{}{}".format( 2317 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2318 company, 2319 ), 2320 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2321 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2322 )) 2323 2324 info.extend([ 2325 "\n## Portfolio distribution by sectors\n" 2326 "\n| Sector | Percent | Current cost |\n", 2327 aSepLine, 2328 ]) 2329 2330 for sector in view["analytics"]["distrBySectors"].keys(): 2331 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2332 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2333 sector, 2334 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2335 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2336 )) 2337 2338 info.extend([ 2339 "\n## Portfolio distribution by currencies\n" 2340 "\n| Instruments currencies | Percent | Current cost |\n", 2341 aSepLine, 2342 ]) 2343 2344 for curr in view["analytics"]["distrByCurrencies"].keys(): 2345 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2346 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2347 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2348 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2349 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2350 )) 2351 2352 info.extend([ 2353 "\n## Portfolio distribution by countries\n" 2354 "\n| Assets by country | Percent | Current cost |\n", 2355 aSepLine, 2356 ]) 2357 2358 for country in view["analytics"]["distrByCountries"].keys(): 2359 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2360 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2361 country, 2362 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2363 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2364 )) 2365 2366 if details in ["full", "calendar"]: 2367 # -- Show bonds payment calendar section: 2368 if view["stat"]["Bonds"]: 2369 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2370 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2371 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2372 2373 else: 2374 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2375 2376 infoText = "".join(info) 2377 2378 uLogger.info(infoText) 2379 2380 if details == "full" and self.overviewFile: 2381 filename = self.overviewFile 2382 2383 elif details == "digest" and self.overviewDigestFile: 2384 filename = self.overviewDigestFile 2385 2386 elif details == "positions" and self.overviewPositionsFile: 2387 filename = self.overviewPositionsFile 2388 2389 elif details == "orders" and self.overviewOrdersFile: 2390 filename = self.overviewOrdersFile 2391 2392 elif details == "analytics" and self.overviewAnalyticsFile: 2393 filename = self.overviewAnalyticsFile 2394 2395 elif details == "calendar" and self.overviewBondsCalendarFile: 2396 filename = self.overviewBondsCalendarFile 2397 2398 else: 2399 filename = "" 2400 2401 if filename: 2402 with open(filename, "w", encoding="UTF-8") as fH: 2403 fH.write(infoText) 2404 2405 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2406 2407 if self.useHTMLReports: 2408 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2409 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2410 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2411 2412 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2413 2414 return view 2415 2416 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2417 """ 2418 Returns history operations between two given dates for current `accountId`. 2419 If `reportFile` string is not empty then also save human-readable report. 2420 Shows some statistical data of closed positions. 2421 2422 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2423 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2424 :param show: if `True` then also prints all records to the console. 2425 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2426 :return: original list of dictionaries with history of deals records from API ("operations" key): 2427 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2428 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2429 """ 2430 if self.accountId is None or not self.accountId: 2431 uLogger.error("Variable `accountId` must be defined for using this method!") 2432 raise Exception("Account ID required") 2433 2434 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2435 2436 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2437 2438 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2439 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2440 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2441 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2442 customStat = {} # custom statistics in additional to responseJSON 2443 2444 # --- output report in human-readable format: 2445 if show or self.reportFile: 2446 splitLine1 = "| | | | | |\n" # Summary section 2447 splitLine2 = "| | | | | | | | |\n" # Operations section 2448 nextDay = "" 2449 2450 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2451 2452 if len(ops) > 0: 2453 customStat = { 2454 "opsCount": 0, # total operations count 2455 "buyCount": 0, # buy operations 2456 "sellCount": 0, # sell operations 2457 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2458 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2459 "payIn": {"rub": 0.}, # Deposit brokerage account 2460 "payOut": {"rub": 0.}, # Withdrawals 2461 "divs": {"rub": 0.}, # Dividends income 2462 "coupons": {"rub": 0.}, # Coupon's income 2463 "brokerCom": {"rub": 0.}, # Service commissions 2464 "serviceCom": {"rub": 0.}, # Service commissions 2465 "marginCom": {"rub": 0.}, # Margin commissions 2466 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2467 } 2468 2469 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2470 for item in ops: 2471 if item["state"] == "OPERATION_STATE_EXECUTED": 2472 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2473 2474 # count buy operations: 2475 if "_BUY" in item["operationType"]: 2476 customStat["buyCount"] += 1 2477 2478 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2479 customStat["buyTotal"][item["payment"]["currency"]] += payment 2480 2481 else: 2482 customStat["buyTotal"][item["payment"]["currency"]] = payment 2483 2484 # count sell operations: 2485 elif "_SELL" in item["operationType"]: 2486 customStat["sellCount"] += 1 2487 2488 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2489 customStat["sellTotal"][item["payment"]["currency"]] += payment 2490 2491 else: 2492 customStat["sellTotal"][item["payment"]["currency"]] = payment 2493 2494 # count incoming operations: 2495 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2496 if item["payment"]["currency"] in customStat["payIn"].keys(): 2497 customStat["payIn"][item["payment"]["currency"]] += payment 2498 2499 else: 2500 customStat["payIn"][item["payment"]["currency"]] = payment 2501 2502 # count withdrawals operations: 2503 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2504 if item["payment"]["currency"] in customStat["payOut"].keys(): 2505 customStat["payOut"][item["payment"]["currency"]] += payment 2506 2507 else: 2508 customStat["payOut"][item["payment"]["currency"]] = payment 2509 2510 # count dividends income: 2511 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2512 if item["payment"]["currency"] in customStat["divs"].keys(): 2513 customStat["divs"][item["payment"]["currency"]] += payment 2514 2515 else: 2516 customStat["divs"][item["payment"]["currency"]] = payment 2517 2518 # count coupon's income: 2519 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2520 if item["payment"]["currency"] in customStat["coupons"].keys(): 2521 customStat["coupons"][item["payment"]["currency"]] += payment 2522 2523 else: 2524 customStat["coupons"][item["payment"]["currency"]] = payment 2525 2526 # count broker commissions: 2527 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2528 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2529 customStat["brokerCom"][item["payment"]["currency"]] += payment 2530 2531 else: 2532 customStat["brokerCom"][item["payment"]["currency"]] = payment 2533 2534 # count service commissions: 2535 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2536 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2537 customStat["serviceCom"][item["payment"]["currency"]] += payment 2538 2539 else: 2540 customStat["serviceCom"][item["payment"]["currency"]] = payment 2541 2542 # count margin commissions: 2543 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2544 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2545 customStat["marginCom"][item["payment"]["currency"]] += payment 2546 2547 else: 2548 customStat["marginCom"][item["payment"]["currency"]] = payment 2549 2550 # count withholding taxes: 2551 elif "_TAX" in item["operationType"]: 2552 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2553 customStat["allTaxes"][item["payment"]["currency"]] += payment 2554 2555 else: 2556 customStat["allTaxes"][item["payment"]["currency"]] = payment 2557 2558 else: 2559 continue 2560 2561 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2562 2563 # --- view "Actions" lines: 2564 info.extend([ 2565 "| Report sections | | | | |\n", 2566 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2567 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2568 "| | Buy: {:<22} | {:<28} | | |\n".format( 2569 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2570 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2571 ), 2572 "| | Sell: {:<21} | {:<28} | | |\n".format( 2573 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2574 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2575 ), 2576 ]) 2577 2578 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2579 for key in opsKeys: 2580 if key == "rub": 2581 continue 2582 2583 info.extend([ 2584 "| | | {:<28} | | |\n".format( 2585 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2586 ), 2587 "| | | {:<28} | | |\n".format( 2588 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2589 ), 2590 ]) 2591 2592 info.append(splitLine1) 2593 2594 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2595 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2596 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2597 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2598 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2599 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2600 ) 2601 2602 # --- view "Payments" lines: 2603 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2604 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2605 2606 for key in paymentsKeys: 2607 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2608 2609 info.append(splitLine1) 2610 2611 # --- view "Commissions and taxes" lines: 2612 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2613 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2614 2615 for key in comKeys: 2616 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2617 2618 info.extend([ 2619 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2620 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2621 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2622 ]) 2623 2624 else: 2625 info.append("Broker returned no operations during this period\n") 2626 2627 # --- view "Operations" section: 2628 for item in ops: 2629 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2630 continue 2631 2632 else: 2633 self._figi = item["figi"] 2634 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2635 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2636 2637 # group of deals during one day: 2638 if nextDay and item["date"].split("T")[0] != nextDay: 2639 info.append(splitLine2) 2640 nextDay = "" 2641 2642 else: 2643 nextDay = item["date"].split("T")[0] # saving current day for splitting 2644 2645 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2646 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2647 self._figi if self._figi else "—", 2648 instrument["ticker"] if instrument else "—", 2649 instrument["type"] if instrument else "—", 2650 item["quantity"] if int(item["quantity"]) > 0 else "—", 2651 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2652 TKS_OPERATION_STATES[item["state"]], 2653 TKS_OPERATION_TYPES[item["operationType"]], 2654 )) 2655 2656 infoText = "".join(info) 2657 2658 if show: 2659 if self.moreDebug: 2660 uLogger.debug("Records about history of a client's operations successfully received") 2661 2662 uLogger.info(infoText) 2663 2664 if self.reportFile: 2665 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2666 fH.write(infoText) 2667 2668 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2669 2670 if self.useHTMLReports: 2671 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2672 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2673 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2674 2675 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2676 2677 return ops, customStat 2678 2679 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2680 """ 2681 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2682 2683 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2684 Warning! Broker server used ISO UTC time by default. 2685 2686 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2687 Also, `historyFile` used to update history with `onlyMissing` parameter. 2688 2689 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2690 2691 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2692 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2693 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2694 `"hour"`, `"day"`. Default: `"hour"`. 2695 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2696 False by default. Warning! History appends only from last candle to current time 2697 with always update last candle! 2698 :param csvSep: separator if csv-file is used, `,` by default. 2699 :param show: if `True` then also prints Pandas DataFrame to the console. 2700 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2701 `["date", "time", "open", "high", "low", "close", "volume"]`. 2702 """ 2703 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2704 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2705 history = None # empty pandas object for history 2706 2707 if interval not in TKS_CANDLE_INTERVALS.keys(): 2708 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2709 raise Exception("Incorrect value") 2710 2711 if not (self._ticker or self._figi): 2712 uLogger.error("Ticker or FIGI must be defined!") 2713 raise Exception("Ticker or FIGI required") 2714 2715 if self._ticker and not self._figi: 2716 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2717 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2718 2719 if self._figi and not self._ticker: 2720 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2721 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2722 2723 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2724 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2725 if interval.lower() != "day": 2726 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2727 2728 delta = dtEnd - dtStart # current UTC time minus last time in file 2729 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2730 2731 # calculate history length in candles: 2732 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2733 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2734 length += 1 # to avoid fraction time 2735 2736 # calculate data blocks count: 2737 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2738 2739 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2740 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2741 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2742 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2743 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2744 2745 tempOld = None # pandas object for old history, if --only-missing key present 2746 lastTime = None # datetime object of last old candle in file 2747 2748 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2749 uLogger.debug("--only-missing key present, add only last missing candles...") 2750 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2751 2752 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2753 2754 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2755 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2756 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2757 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2758 2759 # get last datetime object from last string in file or minus 1 delta if file is empty: 2760 if len(tempOld) > 0: 2761 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2762 2763 else: 2764 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2765 2766 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2767 2768 responseJSONs = [] # raw history blocks of data 2769 2770 blockEnd = dtEnd 2771 for item in range(blocks): 2772 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2773 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2774 2775 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2776 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2777 )) 2778 2779 if blockStart == blockEnd: 2780 uLogger.debug("Skipped this zero-length block...") 2781 2782 else: 2783 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2784 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2785 self.body = str({ 2786 "figi": self._figi, 2787 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2788 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2789 "interval": TKS_CANDLE_INTERVALS[interval][0] 2790 }) 2791 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2792 2793 if "code" in responseJSON.keys(): 2794 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2795 2796 else: 2797 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2798 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2799 2800 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2801 2802 blockEnd = blockStart 2803 2804 printCount = len(responseJSONs) # candles to show in console 2805 if responseJSONs: 2806 tempHistory = pd.DataFrame( 2807 data={ 2808 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2809 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2810 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2811 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2812 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2813 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2814 "volume": [int(item["volume"]) for item in responseJSONs], 2815 }, 2816 index=range(len(responseJSONs)), 2817 columns=["date", "time", "open", "high", "low", "close", "volume"], 2818 ) 2819 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2820 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2821 2822 # append only newest candles to old history if --only-missing key present: 2823 if onlyMissing and tempOld is not None and lastTime is not None: 2824 index = 0 # find start index in tempHistory data: 2825 2826 for i, item in tempHistory.iterrows(): 2827 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2828 2829 if curTime == lastTime: 2830 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2831 index = i 2832 printCount = index + 1 2833 break 2834 2835 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2836 2837 else: 2838 history = tempHistory # if no `--only-missing` key then load full data from server 2839 2840 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2841 2842 if history is not None and not history.empty: 2843 if show: 2844 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2845 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2846 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2847 )) 2848 2849 else: 2850 uLogger.warning("Received an empty candles history!") 2851 2852 if self.historyFile is not None: 2853 if history is not None and not history.empty: 2854 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2855 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2856 2857 else: 2858 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2859 2860 else: 2861 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2862 2863 return history 2864 2865 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2866 """ 2867 Load candles history from csv-file and return Pandas DataFrame object. 2868 2869 See also: `History()` and `ShowHistoryChart()` methods. 2870 2871 :param filePath: path to csv-file to open. 2872 """ 2873 loadedHistory = None # init candles data object 2874 2875 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2876 2877 if os.path.exists(filePath): 2878 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2879 2880 tfStr = self.priceModel.FormattedDelta( 2881 self.priceModel.timeframe, 2882 "{days} days {hours}h {minutes}m {seconds}s", 2883 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2884 self.priceModel.timeframe, 2885 "{hours}h {minutes}m {seconds}s", 2886 ) 2887 2888 if loadedHistory is not None and not loadedHistory.empty: 2889 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2890 len(loadedHistory), 2891 tfStr, 2892 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2893 ) 2894 2895 else: 2896 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2897 2898 else: 2899 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2900 2901 return loadedHistory 2902 2903 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2904 """ 2905 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2906 2907 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2908 Default: `index.html` (both for interact and non-interact candlesticks chart). 2909 2910 See also: `History()` and `LoadHistory()` methods. 2911 2912 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2913 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2914 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2915 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2916 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2917 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2918 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2919 """ 2920 if isinstance(candles, str): 2921 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2922 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2923 2924 elif isinstance(candles, pd.DataFrame): 2925 self.priceModel.prices = candles # set candles chain from variable 2926 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2927 2928 if "datetime" not in candles.columns: 2929 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2930 2931 else: 2932 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2933 raise Exception("Incorrect value") 2934 2935 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2936 2937 if interact: 2938 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2939 2940 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2941 2942 else: 2943 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2944 2945 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2946 2947 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2948 2949 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2950 """ 2951 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2952 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2953 2954 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2955 2956 :param operation: string "Buy" or "Sell". 2957 :param lots: volume, integer count of lots >= 1. 2958 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2959 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2960 :param expDate: string "Undefined" by default or local date in future, 2961 it is a string with format `%Y-%m-%d %H:%M:%S`. 2962 :return: JSON with response from broker server. 2963 """ 2964 if self.accountId is None or not self.accountId: 2965 uLogger.error("Variable `accountId` must be defined for using this method!") 2966 raise Exception("Account ID required") 2967 2968 if operation is None or not operation or operation not in ("Buy", "Sell"): 2969 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2970 raise Exception("Incorrect value") 2971 2972 if lots is None or lots < 1: 2973 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2974 lots = 1 2975 2976 if tp is None or tp < 0: 2977 tp = 0 2978 2979 if sl is None or sl < 0: 2980 sl = 0 2981 2982 if expDate is None or not expDate: 2983 expDate = "Undefined" 2984 2985 if not (self._ticker or self._figi): 2986 uLogger.error("Ticker or FIGI must be defined!") 2987 raise Exception("Ticker or FIGI required") 2988 2989 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2990 self._ticker = instrument["ticker"] 2991 self._figi = instrument["figi"] 2992 2993 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2994 2995 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2996 self.body = str({ 2997 "figi": self._figi, 2998 "quantity": str(lots), 2999 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3000 "accountId": str(self.accountId), 3001 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3002 }) 3003 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3004 3005 if "orderId" in response.keys(): 3006 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3007 operation, response["orderId"], 3008 self._ticker, self._figi, lots, 3009 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3010 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3011 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3012 )) 3013 3014 if tp > 0: 3015 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3016 3017 if sl > 0: 3018 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3019 3020 else: 3021 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3022 3023 return response 3024 3025 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3026 """ 3027 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3028 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3029 3030 See also: `Order()` and `Trade()` docstrings. 3031 3032 :param lots: volume, integer count of lots >= 1. 3033 :param tp: float > 0, take profit price of stop-order. 3034 :param sl: float > 0, stop loss price of stop-order. 3035 :param expDate: it's a local date in future. 3036 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3037 :return: JSON with response from broker server. 3038 """ 3039 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3040 3041 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3042 """ 3043 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3044 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3045 3046 See also: `Order()` and `Trade()` docstrings. 3047 3048 :param lots: volume, integer count of lots >= 1. 3049 :param tp: float > 0, take profit price of stop-order. 3050 :param sl: float > 0, stop loss price of stop-order. 3051 :param expDate: it's a local date in the future. 3052 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3053 :return: JSON with response from broker server. 3054 """ 3055 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3056 3057 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3058 """ 3059 Close position of given instruments. 3060 3061 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3062 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3063 This avoids unnecessary downloading data from the server. 3064 """ 3065 if instruments is None or not instruments: 3066 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3067 raise Exception("Ticker or FIGI required") 3068 3069 if isinstance(instruments, str): 3070 instruments = [instruments] 3071 3072 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3073 if uniqueInstruments: 3074 if portfolio is None or not portfolio: 3075 portfolio = self.Overview(show=False) 3076 3077 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3078 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3079 3080 for self._figi in uniqueInstruments: 3081 if self._figi not in allOpened: 3082 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3083 continue 3084 3085 # search open trade info about instrument by ticker: 3086 instrument = {} 3087 for iType in TKS_INSTRUMENTS: 3088 if instrument: 3089 break 3090 3091 for item in portfolio["stat"][iType]: 3092 if item["figi"] == self._figi: 3093 instrument = item 3094 break 3095 3096 if instrument: 3097 self._ticker = instrument["ticker"] 3098 self._figi = instrument["figi"] 3099 3100 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3101 self._ticker, 3102 self._figi, 3103 int(instrument["volume"]), 3104 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3105 )) 3106 3107 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3108 3109 if tradeLots > 0: 3110 if instrument["blocked"] > 0: 3111 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3112 instrument["blocked"], 3113 self._ticker, 3114 tradeLots, 3115 )) 3116 3117 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3118 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3119 3120 else: 3121 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3122 3123 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3124 """ 3125 Close all positions of given instruments with defined type. 3126 3127 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3128 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3129 This avoids unnecessary downloading data from the server. 3130 """ 3131 if iType not in TKS_INSTRUMENTS: 3132 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3133 3134 else: 3135 if portfolio is None or not portfolio: 3136 portfolio = self.Overview(show=False) 3137 3138 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3139 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3140 3141 if tickers and portfolio: 3142 self.CloseTrades(tickers, portfolio) 3143 3144 else: 3145 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3146 3147 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3148 """ 3149 Universal method to create market or limit orders with all available parameters for current `accountId`. 3150 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3151 3152 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3153 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3154 3155 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3156 then broker immediately open market order as you can do simple --buy or --sell operations! 3157 3158 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3159 When current price will go up or down to target price value then broker opens a limit order. 3160 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3161 3162 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3163 3164 :param operation: string "Buy" or "Sell". 3165 :param orderType: string "Limit" or "Stop". 3166 :param lots: volume, integer count of lots >= 1. 3167 :param targetPrice: target price > 0. This is open trade price for limit order. 3168 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3169 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3170 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3171 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3172 Stop loss order always executed by market price. 3173 :param expDate: string "Undefined" by default or local date in future. 3174 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3175 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3176 A limit order has no expiration date, it lasts until the end of the trading day. 3177 :return: JSON with response from broker server. 3178 """ 3179 if self.accountId is None or not self.accountId: 3180 uLogger.error("Variable `accountId` must be defined for using this method!") 3181 raise Exception("Account ID required") 3182 3183 if operation is None or not operation or operation not in ("Buy", "Sell"): 3184 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3185 raise Exception("Incorrect value") 3186 3187 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3188 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3189 raise Exception("Incorrect value") 3190 3191 if lots is None or lots < 1: 3192 uLogger.error("You must define trade volume > 0: integer count of lots!") 3193 raise Exception("Incorrect value") 3194 3195 if targetPrice is None or targetPrice <= 0: 3196 uLogger.error("Target price for limit-order must be greater than 0!") 3197 raise Exception("Incorrect value") 3198 3199 if limitPrice is None or limitPrice <= 0: 3200 limitPrice = targetPrice 3201 3202 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3203 stopType = "Limit" 3204 3205 if expDate is None or not expDate: 3206 expDate = "Undefined" 3207 3208 if not (self._ticker or self._figi): 3209 uLogger.error("Tocker or FIGI must be defined!") 3210 raise Exception("Ticker or FIGI required") 3211 3212 response = {} 3213 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3214 self._ticker = instrument["ticker"] 3215 self._figi = instrument["figi"] 3216 3217 if orderType == "Limit": 3218 uLogger.debug( 3219 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3220 self._ticker, self._figi, 3221 operation, lots, targetPrice, instrument["currency"], 3222 )) 3223 3224 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3225 self.body = str({ 3226 "figi": self._figi, 3227 "quantity": str(lots), 3228 "price": FloatToNano(targetPrice), 3229 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3230 "accountId": str(self.accountId), 3231 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3232 }) 3233 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3234 3235 if "orderId" in response.keys(): 3236 uLogger.info( 3237 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3238 response["orderId"], self._ticker, self._figi, operation, lots, 3239 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3240 )) 3241 3242 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3243 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3244 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3245 targetPrice, instrument["currency"], 3246 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3247 )) 3248 3249 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3250 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3251 targetPrice, instrument["currency"], 3252 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3253 )) 3254 3255 else: 3256 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3257 3258 if orderType == "Stop": 3259 uLogger.debug( 3260 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3261 self._ticker, self._figi, 3262 operation, lots, 3263 targetPrice, instrument["currency"], 3264 limitPrice, instrument["currency"], 3265 stopType, expDate, 3266 )) 3267 3268 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3269 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3270 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3271 3272 body = { 3273 "figi": self._figi, 3274 "quantity": str(lots), 3275 "price": FloatToNano(limitPrice), 3276 "stopPrice": FloatToNano(targetPrice), 3277 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3278 "accountId": str(self.accountId), 3279 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3280 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3281 } 3282 3283 if expDateUTC: 3284 body["expireDate"] = expDateUTC 3285 3286 self.body = str(body) 3287 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3288 3289 if "stopOrderId" in response.keys(): 3290 uLogger.info( 3291 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3292 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3293 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3294 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3295 TKS_STOP_ORDER_TYPES[stopOrderType], 3296 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3297 )) 3298 3299 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3300 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3301 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3302 targetPrice, instrument["currency"], 3303 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3304 )) 3305 3306 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3307 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3308 targetPrice, instrument["currency"], 3309 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3310 )) 3311 3312 else: 3313 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3314 3315 return response 3316 3317 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3318 """ 3319 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3320 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3321 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3322 See also: `Order()` docstring. 3323 3324 :param lots: volume, integer count of lots >= 1. 3325 :param targetPrice: target price > 0. This is open trade price for limit order. 3326 :return: JSON with response from broker server. 3327 """ 3328 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3329 3330 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3331 """ 3332 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3333 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3334 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3335 target price value then broker opens a limit order. See also: `Order()` docstring. 3336 3337 :param lots: volume, integer count of lots >= 1. 3338 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3339 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3340 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3341 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3342 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3343 :param expDate: string "Undefined" by default or local date in future. 3344 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3345 This date is converting to UTC format for server. 3346 :return: JSON with response from broker server. 3347 """ 3348 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3349 3350 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3351 """ 3352 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3353 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3354 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3355 See also: `Order()` docstring. 3356 3357 :param lots: volume, integer count of lots >= 1. 3358 :param targetPrice: target price > 0. This is open trade price for limit order. 3359 :return: JSON with response from broker server. 3360 """ 3361 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3362 3363 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3364 """ 3365 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3366 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3367 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3368 target price value then broker opens a limit order. See also: `Order()` docstring. 3369 3370 :param lots: volume, integer count of lots >= 1. 3371 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3372 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3373 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3374 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3375 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3376 :param expDate: string "Undefined" by default or local date in future. 3377 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3378 This date is converting to UTC format for server. 3379 :return: JSON with response from broker server. 3380 """ 3381 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3382 3383 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3384 """ 3385 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3386 3387 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3388 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3389 This avoids unnecessary downloading data from the server. 3390 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3391 """ 3392 if self.accountId is None or not self.accountId: 3393 uLogger.error("Variable `accountId` must be defined for using this method!") 3394 raise Exception("Account ID required") 3395 3396 if orderIDs: 3397 if allOrdersIDs is None: 3398 rawOrders = self.RequestPendingOrders() 3399 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3400 3401 if allStopOrdersIDs is None: 3402 rawStopOrders = self.RequestStopOrders() 3403 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3404 3405 for orderID in orderIDs: 3406 idInPendingOrders = orderID in allOrdersIDs 3407 idInStopOrders = orderID in allStopOrdersIDs 3408 3409 if not (idInPendingOrders or idInStopOrders): 3410 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3411 continue 3412 3413 else: 3414 if idInPendingOrders: 3415 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3416 3417 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3418 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3419 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3420 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3421 3422 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3423 if self.moreDebug: 3424 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3425 3426 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3427 3428 else: 3429 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3430 3431 elif idInStopOrders: 3432 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3433 3434 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3435 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3436 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3437 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3438 3439 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3440 if self.moreDebug: 3441 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3442 3443 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3444 3445 else: 3446 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3447 3448 else: 3449 continue 3450 3451 def CloseAllOrders(self) -> None: 3452 """ 3453 Gets a list of open pending and stop orders and cancel it all. 3454 """ 3455 rawOrders = self.RequestPendingOrders() 3456 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3457 lenOrders = len(allOrdersIDs) 3458 3459 rawStopOrders = self.RequestStopOrders() 3460 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3461 lenSOrders = len(allStopOrdersIDs) 3462 3463 if lenOrders > 0 or lenSOrders > 0: 3464 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3465 3466 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3467 3468 else: 3469 uLogger.info("Orders not found, nothing to cancel.") 3470 3471 def CloseAll(self, *args) -> None: 3472 """ 3473 Close all available (not blocked) opened trades and orders. 3474 3475 Also, you can select one or more keywords case-insensitive: 3476 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3477 3478 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3479 """ 3480 overview = self.Overview(show=False) # get all open trades info 3481 3482 if len(args) == 0: 3483 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3484 self.CloseAllOrders() # close all pending and stop orders 3485 3486 for iType in TKS_INSTRUMENTS: 3487 if iType != "Currencies": 3488 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3489 3490 else: 3491 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3492 lowerArgs = [x.lower() for x in args] 3493 3494 if "orders" in lowerArgs: 3495 self.CloseAllOrders() # close all pending and stop orders 3496 3497 for iType in TKS_INSTRUMENTS: 3498 if iType.lower() in lowerArgs and iType != "Currencies": 3499 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3500 3501 def CloseAllByTicker(self, instrument: str) -> None: 3502 """ 3503 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3504 3505 This method searches opened trade and orders of instrument throw all portfolio and then use 3506 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3507 3508 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3509 3510 :param instrument: string with ticker. 3511 """ 3512 if instrument is None or not instrument: 3513 uLogger.error("Ticker name must be defined for using this method!") 3514 raise Exception("Ticker required") 3515 3516 overview = self.Overview(show=False) # get user portfolio with all open trades info 3517 3518 self._ticker = instrument # try to set instrument as ticker 3519 self._figi = "" 3520 3521 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3522 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3523 3524 if limitAll and self.IsInLimitOrders(portfolio=overview): 3525 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3526 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3527 3528 if stopAll and self.IsInStopOrders(portfolio=overview): 3529 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3530 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3531 3532 if self.IsInPortfolio(portfolio=overview): 3533 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3534 self.CloseTrades(instruments=[instrument], portfolio=overview) 3535 3536 def CloseAllByFIGI(self, instrument: str) -> None: 3537 """ 3538 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3539 3540 This method searches opened trade and orders of instrument throw all portfolio and then use 3541 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3542 3543 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3544 3545 :param instrument: string with FIGI id. 3546 """ 3547 if instrument is None or not instrument: 3548 uLogger.error("FIGI id must be defined for using this method!") 3549 raise Exception("FIGI required") 3550 3551 overview = self.Overview(show=False) # get user portfolio with all open trades info 3552 3553 self._ticker = "" 3554 self._figi = instrument # try to set instrument as FIGI id 3555 3556 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3557 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3558 3559 if limitAll and self.IsInLimitOrders(portfolio=overview): 3560 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3561 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3562 3563 if stopAll and self.IsInStopOrders(portfolio=overview): 3564 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3565 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3566 3567 if self.IsInPortfolio(portfolio=overview): 3568 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3569 self.CloseTrades(instruments=[instrument], portfolio=overview) 3570 3571 @staticmethod 3572 def ParseOrderParameters(operation, **inputParameters): 3573 """ 3574 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3575 3576 :param operation: string "Buy" or "Sell". 3577 :param inputParameters: this is dict of strings that looks like this 3578 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3579 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3580 "prices" key: one or more prices to open limit-orders 3581 Counts of values in lots and prices lists must be equals! 3582 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3583 """ 3584 # TODO: update order grid work with api v2 3585 pass 3586 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3587 # 3588 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3589 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3590 # raise Exception("Incorrect value") 3591 # 3592 # if "l" in inputParameters.keys(): 3593 # inputParameters["lots"] = inputParameters.pop("l") 3594 # 3595 # if "p" in inputParameters.keys(): 3596 # inputParameters["prices"] = inputParameters.pop("p") 3597 # 3598 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3599 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3600 # raise Exception("Incorrect value") 3601 # 3602 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3603 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3604 # 3605 # if len(lots) != len(prices): 3606 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3607 # raise Exception("Incorrect value") 3608 # 3609 # uLogger.debug("Extracted parameters for orders:") 3610 # uLogger.debug("lots = {}".format(lots)) 3611 # uLogger.debug("prices = {}".format(prices)) 3612 # 3613 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3614 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3615 # uLogger.debug("Order parameters: {}".format(result)) 3616 # 3617 # return result 3618 3619 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3620 """ 3621 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3622 3623 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3624 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3625 """ 3626 result = False 3627 msg = "Instrument not defined!" 3628 3629 if portfolio is None or not portfolio: 3630 portfolio = self.Overview(show=False) 3631 3632 if self._ticker: 3633 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3634 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3635 3636 for iType in TKS_INSTRUMENTS: 3637 for instrument in portfolio["stat"][iType]: 3638 if instrument["ticker"] == self._ticker: 3639 result = True 3640 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3641 break 3642 3643 elif self._figi: 3644 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3645 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3646 3647 for iType in TKS_INSTRUMENTS: 3648 for instrument in portfolio["stat"][iType]: 3649 if instrument["figi"] == self._figi: 3650 result = True 3651 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3652 break 3653 3654 else: 3655 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3656 3657 uLogger.debug(msg) 3658 3659 return result 3660 3661 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3662 """ 3663 Returns instrument from the user's portfolio if it presents there. 3664 Instrument must be defined by `ticker` (highly priority) or `figi`. 3665 3666 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3667 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3668 """ 3669 result = None 3670 msg = "Instrument not defined!" 3671 3672 if portfolio is None or not portfolio: 3673 portfolio = self.Overview(show=False) 3674 3675 if self._ticker: 3676 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3677 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3678 3679 for iType in TKS_INSTRUMENTS: 3680 for instrument in portfolio["stat"][iType]: 3681 if instrument["ticker"] == self._ticker: 3682 result = instrument 3683 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3684 break 3685 3686 elif self._figi: 3687 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3688 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3689 3690 for iType in TKS_INSTRUMENTS: 3691 for instrument in portfolio["stat"][iType]: 3692 if instrument["figi"] == self._figi: 3693 result = instrument 3694 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3695 break 3696 3697 else: 3698 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3699 3700 uLogger.debug(msg) 3701 3702 return result 3703 3704 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3705 """ 3706 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3707 3708 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3709 3710 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3711 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3712 """ 3713 result = False 3714 msg = "Instrument not defined!" 3715 3716 if portfolio is None or not portfolio: 3717 portfolio = self.Overview(show=False) 3718 3719 if self._ticker: 3720 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3721 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3722 3723 for instrument in portfolio["stat"]["orders"]: 3724 if instrument["ticker"] == self._ticker: 3725 result = True 3726 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3727 break 3728 3729 elif self._figi: 3730 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3731 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3732 3733 for instrument in portfolio["stat"]["orders"]: 3734 if instrument["figi"] == self._figi: 3735 result = True 3736 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3737 break 3738 3739 else: 3740 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3741 3742 uLogger.debug(msg) 3743 3744 return result 3745 3746 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3747 """ 3748 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3749 Instrument must be defined by `ticker` (highly priority) or `figi`. 3750 3751 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3752 3753 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3754 :return: list with `orderID`s of limit orders. 3755 """ 3756 result = [] 3757 msg = "Instrument not defined!" 3758 3759 if portfolio is None or not portfolio: 3760 portfolio = self.Overview(show=False) 3761 3762 if self._ticker: 3763 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3764 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3765 3766 for instrument in portfolio["stat"]["orders"]: 3767 if instrument["ticker"] == self._ticker: 3768 result.append(instrument["orderID"]) 3769 3770 if result: 3771 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3772 3773 elif self._figi: 3774 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3775 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3776 3777 for instrument in portfolio["stat"]["orders"]: 3778 if instrument["figi"] == self._figi: 3779 result.append(instrument["orderID"]) 3780 3781 if result: 3782 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3783 3784 else: 3785 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3786 3787 uLogger.debug(msg) 3788 3789 return result 3790 3791 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3792 """ 3793 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3794 3795 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3796 3797 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3798 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3799 """ 3800 result = False 3801 msg = "Instrument not defined!" 3802 3803 if portfolio is None or not portfolio: 3804 portfolio = self.Overview(show=False) 3805 3806 if self._ticker: 3807 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3808 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3809 3810 for instrument in portfolio["stat"]["stopOrders"]: 3811 if instrument["ticker"] == self._ticker: 3812 result = True 3813 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3814 break 3815 3816 elif self._figi: 3817 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3818 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3819 3820 for instrument in portfolio["stat"]["stopOrders"]: 3821 if instrument["figi"] == self._figi: 3822 result = True 3823 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3824 break 3825 3826 else: 3827 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3828 3829 uLogger.debug(msg) 3830 3831 return result 3832 3833 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3834 """ 3835 Returns list with all `orderID`s of opened stop orders for the instrument. 3836 Instrument must be defined by `ticker` (highly priority) or `figi`. 3837 3838 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3839 3840 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3841 :return: list with `orderID`s of stop orders. 3842 """ 3843 result = [] 3844 msg = "Instrument not defined!" 3845 3846 if portfolio is None or not portfolio: 3847 portfolio = self.Overview(show=False) 3848 3849 if self._ticker: 3850 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3851 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3852 3853 for instrument in portfolio["stat"]["stopOrders"]: 3854 if instrument["ticker"] == self._ticker: 3855 result.append(instrument["orderID"]) 3856 3857 if result: 3858 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3859 3860 elif self._figi: 3861 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3862 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3863 3864 for instrument in portfolio["stat"]["stopOrders"]: 3865 if instrument["figi"] == self._figi: 3866 result.append(instrument["orderID"]) 3867 3868 if result: 3869 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3870 3871 else: 3872 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3873 3874 uLogger.debug(msg) 3875 3876 return result 3877 3878 def RequestLimits(self) -> dict: 3879 """ 3880 Method for obtaining the available funds for withdrawal for current `accountId`. 3881 3882 See also: 3883 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3884 - `OverviewLimits()` method 3885 3886 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3887 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3888 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3889 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3890 """ 3891 if self.accountId is None or not self.accountId: 3892 uLogger.error("Variable `accountId` must be defined for using this method!") 3893 raise Exception("Account ID required") 3894 3895 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3896 3897 self.body = str({"accountId": self.accountId}) 3898 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3899 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3900 3901 if self.moreDebug: 3902 uLogger.debug("Records about available funds for withdrawal successfully received") 3903 3904 return rawLimits 3905 3906 def OverviewLimits(self, show: bool = False) -> dict: 3907 """ 3908 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3909 3910 See also: `RequestLimits()`. 3911 3912 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3913 :return: dict with raw parsed data from server and some calculated statistics about it. 3914 """ 3915 if self.accountId is None or not self.accountId: 3916 uLogger.error("Variable `accountId` must be defined for using this method!") 3917 raise Exception("Account ID required") 3918 3919 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3920 3921 view = { 3922 "rawLimits": rawLimits, 3923 "limits": { # parsed data for every currency: 3924 "money": { # this is an array of portfolio currency positions 3925 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3926 }, 3927 "blocked": { # this is an array of blocked currency 3928 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3929 }, 3930 "blockedGuarantee": { # this is locked money under collateral for futures 3931 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3932 }, 3933 }, 3934 } 3935 3936 # --- Prepare text table with limits in human-readable format: 3937 if show: 3938 info = [ 3939 "# Withdrawal limits\n\n", 3940 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3941 "* **Account ID:** [{}]\n".format(self.accountId), 3942 ] 3943 3944 if view["limits"]["money"]: 3945 info.extend([ 3946 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3947 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3948 ]) 3949 3950 else: 3951 info.append("\nNo withdrawal limits\n") 3952 3953 for curr in view["limits"]["money"].keys(): 3954 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3955 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3956 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3957 3958 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3959 "[{}]".format(curr), 3960 "{:.2f}".format(view["limits"]["money"][curr]), 3961 "{:.2f}".format(availableMoney), 3962 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3963 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3964 ) 3965 3966 if curr == "rub": 3967 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3968 3969 else: 3970 info.append(infoStr) 3971 3972 infoText = "".join(info) 3973 3974 uLogger.info(infoText) 3975 3976 if self.withdrawalLimitsFile: 3977 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3978 fH.write(infoText) 3979 3980 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3981 3982 if self.useHTMLReports: 3983 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3984 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3985 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3986 3987 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3988 3989 return view 3990 3991 def RequestAccounts(self) -> dict: 3992 """ 3993 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3994 3995 See also: 3996 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3997 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3998 - `OverviewUserInfo()` method 3999 4000 :return: dict with raw data from server that contains accounts info. Example of dict: 4001 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4002 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4003 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4004 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4005 """ 4006 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4007 4008 self.body = str({}) 4009 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4010 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4011 4012 if self.moreDebug: 4013 uLogger.debug("Records about available accounts successfully received") 4014 4015 return rawAccounts 4016 4017 def RequestUserInfo(self) -> dict: 4018 """ 4019 Method for requesting common user's information. 4020 4021 See also: 4022 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4023 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4024 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4025 - `OverviewUserInfo()` method 4026 4027 :return: dict with raw data from server that contains user's information. Example of dict: 4028 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4029 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4030 """ 4031 uLogger.debug("Requesting common user's information. Wait, please...") 4032 4033 self.body = str({}) 4034 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4035 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4036 4037 if self.moreDebug: 4038 uLogger.debug("Records about current user successfully received") 4039 4040 return rawUserInfo 4041 4042 def RequestMarginStatus(self, accountId: str = None) -> dict: 4043 """ 4044 Method for requesting margin calculation for defined account ID. 4045 4046 See also: 4047 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4048 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4049 - `OverviewUserInfo()` method 4050 4051 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4052 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4053 Example of responses: 4054 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4055 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4056 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4057 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4058 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4059 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4060 """ 4061 if accountId is None or not accountId: 4062 if self.accountId is None or not self.accountId: 4063 uLogger.error("Variable `accountId` must be defined for using this method!") 4064 raise Exception("Account ID required") 4065 4066 else: 4067 accountId = self.accountId # use `self.accountId` (main ID) by default 4068 4069 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4070 4071 self.body = str({"accountId": accountId}) 4072 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4073 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4074 4075 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4076 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4077 rawMargin = {} 4078 4079 else: 4080 if self.moreDebug: 4081 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4082 4083 return rawMargin 4084 4085 def RequestTariffLimits(self) -> dict: 4086 """ 4087 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4088 4089 See also: 4090 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4091 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4092 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4093 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4094 - `OverviewUserInfo()` method 4095 4096 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4097 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4098 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4099 """ 4100 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4101 4102 self.body = str({}) 4103 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4104 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4105 4106 if self.moreDebug: 4107 uLogger.debug("Records with limits of current tariff successfully received") 4108 4109 return rawTariffLimits 4110 4111 def RequestBondCoupons(self, iJSON: dict) -> dict: 4112 """ 4113 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4114 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4115 All dates are in UTC timezone. 4116 4117 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4118 Documentation: 4119 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4120 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4121 4122 See also: `ExtendBondsData()`. 4123 4124 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4125 If raw iJSON is not data of bond then server returns an error [400] with message: 4126 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4127 :return: dictionary with bond payment calendar. Response example 4128 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4129 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4130 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4131 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4132 """ 4133 if iJSON["figi"] is None or not iJSON["figi"]: 4134 uLogger.error("FIGI must be defined for using this method!") 4135 raise Exception("FIGI required") 4136 4137 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4138 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4139 4140 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4141 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4142 self._figi, 4143 startDate, 4144 endDate, 4145 )) 4146 4147 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4148 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4149 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4150 4151 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4152 uLogger.warning("Instrument type is not bond!") 4153 4154 else: 4155 if self.moreDebug: 4156 uLogger.debug("Records about bond payment calendar successfully received") 4157 4158 return calendar 4159 4160 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4161 """ 4162 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4163 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4164 coupon yields, current yields and some statistics etc. 4165 4166 WARNING! This is too long operation if a lot of bonds requested from broker server. 4167 4168 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4169 4170 :param instruments: list of strings with tickers or FIGIs. 4171 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4172 for further used by data scientists or stock analytics. 4173 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4174 In XLSX-file and Pandas DataFrame fields mean: 4175 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4176 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4177 """ 4178 if instruments is None or not instruments: 4179 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4180 raise Exception("Ticker or FIGI required") 4181 4182 if isinstance(instruments, str): 4183 instruments = [instruments] 4184 4185 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4186 4187 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4188 4189 iCount = len(uniqueInstruments) 4190 tooLong = iCount >= 20 4191 if tooLong: 4192 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4193 4194 bonds = None 4195 for i, self._figi in enumerate(uniqueInstruments): 4196 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4197 4198 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4199 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4200 rawBond = self.SearchByFIGI(requestPrice=True) 4201 4202 # Widen raw data with UTC current time (iData["actualDateTime"]): 4203 actualDate = datetime.now(tzutc()) 4204 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4205 4206 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4207 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4208 4209 # Replace some values with human-readable: 4210 iData["nominalCurrency"] = iData["nominal"]["currency"] 4211 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4212 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4213 iData["aciCurrency"] = iData["aciValue"]["currency"] 4214 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4215 iData["issueSize"] = int(iData["issueSize"]) 4216 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4217 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4218 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4219 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4220 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4221 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4222 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4223 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4224 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4225 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4226 4227 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4228 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4229 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4230 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4231 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4232 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4233 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4234 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4235 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4236 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4237 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4238 4239 # Widen raw data with calendar data from `rawCalendar` values: 4240 calendarData = [] 4241 if "events" in iData["rawCalendar"].keys(): 4242 for item in iData["rawCalendar"]["events"]: 4243 calendarData.append({ 4244 "couponDate": item["couponDate"], 4245 "couponNumber": int(item["couponNumber"]), 4246 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4247 "payCurrency": item["payOneBond"]["currency"], 4248 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4249 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4250 "couponStartDate": item["couponStartDate"], 4251 "couponEndDate": item["couponEndDate"], 4252 "couponPeriod": item["couponPeriod"], 4253 }) 4254 4255 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4256 if "maturityDate" not in iData.keys(): 4257 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4258 4259 # Widen raw data with Coupon Rate. 4260 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4261 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4262 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4263 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4264 4265 # Widen raw data with Yield to Maturity (YTM) on current date. 4266 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4267 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4268 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4269 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4270 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4271 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4272 4273 iData["calendar"] = calendarData # adds calendar at the end 4274 4275 # Remove not used data: 4276 iData.pop("uid") 4277 iData.pop("positionUid") 4278 iData.pop("currentPrice") 4279 iData.pop("rawCalendar") 4280 4281 colNames = list(iData.keys()) 4282 if bonds is None: 4283 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4284 4285 else: 4286 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4287 4288 else: 4289 uLogger.warning("Instrument is not a bond!") 4290 4291 processed = round(100 * (i + 1) / iCount, 1) 4292 if tooLong and processed % 5 == 0: 4293 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4294 4295 else: 4296 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4297 4298 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4299 4300 # Saving bonds from Pandas DataFrame to XLSX sheet: 4301 if xlsx and self.bondsXLSXFile: 4302 with pd.ExcelWriter( 4303 path=self.bondsXLSXFile, 4304 date_format=TKS_DATE_FORMAT, 4305 datetime_format=TKS_DATE_TIME_FORMAT, 4306 mode="w", 4307 ) as writer: 4308 bonds.to_excel( 4309 writer, 4310 sheet_name="Extended bonds data", 4311 index=True, 4312 encoding="UTF-8", 4313 freeze_panes=(1, 1), 4314 ) # saving as XLSX-file with freeze first row and column as headers 4315 4316 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4317 4318 return bonds 4319 4320 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4321 """ 4322 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4323 4324 WARNING! This is too long operation if a lot of bonds requested from broker server. 4325 4326 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4327 4328 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4329 extended information about bonds: main info, current prices, bond payment calendar, 4330 coupon yields, current yields and some statistics etc. 4331 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4332 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4333 for further used by data scientists or stock analytics. 4334 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4335 """ 4336 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4337 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4338 4339 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4340 4341 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4342 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4343 calendar = None 4344 for bond in extBonds.iterrows(): 4345 for item in bond[1]["calendar"]: 4346 cData = { 4347 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4348 "couponDate": item["couponDate"], 4349 "figi": bond[1]["figi"], 4350 "ticker": bond[1]["ticker"], 4351 "name": bond[1]["name"], 4352 "couponNumber": item["couponNumber"], 4353 "payOneBond": item["payOneBond"], 4354 "payCurrency": item["payCurrency"], 4355 "couponType": item["couponType"], 4356 "couponPeriod": item["couponPeriod"], 4357 "fixDate": item["fixDate"], 4358 "couponStartDate": item["couponStartDate"], 4359 "couponEndDate": item["couponEndDate"], 4360 } 4361 4362 if calendar is None: 4363 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4364 4365 else: 4366 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4367 4368 if calendar is not None: 4369 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4370 4371 # Saving calendar from Pandas DataFrame to XLSX sheet: 4372 if xlsx: 4373 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4374 4375 with pd.ExcelWriter( 4376 path=xlsxCalendarFile, 4377 date_format=TKS_DATE_FORMAT, 4378 datetime_format=TKS_DATE_TIME_FORMAT, 4379 mode="w", 4380 ) as writer: 4381 humanReadable = calendar.copy(deep=True) 4382 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4383 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4384 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4385 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4386 humanReadable.columns = colNames # human-readable column names 4387 4388 humanReadable.to_excel( 4389 writer, 4390 sheet_name="Bond payments calendar", 4391 index=False, 4392 encoding="UTF-8", 4393 freeze_panes=(1, 2), 4394 ) # saving as XLSX-file with freeze first row and column as headers 4395 4396 del humanReadable # release df in memory 4397 4398 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4399 4400 return calendar 4401 4402 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4403 """ 4404 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4405 Also, creates Markdown file with calendar data, `calendar.md` by default. 4406 4407 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4408 4409 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4410 extended information about bonds: main info, current prices, bond payment calendar, 4411 coupon yields, current yields and some statistics etc. 4412 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4413 :param show: if `True` then also printing bonds payment calendar to the console, 4414 otherwise save to file `calendarFile` only. `False` by default. 4415 :return: multilines text in Markdown format with bonds payment calendar as a table. 4416 """ 4417 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4418 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4419 4420 infoText = "# Bond payments calendar\n\n" 4421 4422 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4423 4424 if not (calendar is None or calendar.empty): 4425 splitLine = "| | | | | | | | | |\n" 4426 4427 info = [ 4428 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4429 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4430 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4431 ] 4432 4433 newMonth = False 4434 notOneBond = calendar["figi"].nunique() > 1 4435 for i, bond in enumerate(calendar.iterrows()): 4436 if newMonth and notOneBond: 4437 info.append(splitLine) 4438 4439 info.append( 4440 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4441 " √" if bond[1]["paid"] else " —", 4442 bond[1]["couponDate"].split("T")[0], 4443 bond[1]["figi"], 4444 bond[1]["ticker"], 4445 bond[1]["couponNumber"], 4446 "{} {}".format( 4447 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4448 bond[1]["payCurrency"], 4449 ), 4450 bond[1]["couponType"], 4451 bond[1]["couponPeriod"], 4452 bond[1]["fixDate"].split("T")[0], 4453 ) 4454 ) 4455 4456 if i < len(calendar.values) - 1: 4457 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4458 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4459 newMonth = False if curDate.month == nextDate.month else True 4460 4461 else: 4462 newMonth = False 4463 4464 infoText += "".join(info) 4465 4466 if show: 4467 uLogger.info("{}".format(infoText)) 4468 4469 if self.calendarFile is not None: 4470 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4471 fH.write(infoText) 4472 4473 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4474 4475 if self.useHTMLReports: 4476 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4477 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4478 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4479 4480 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4481 4482 else: 4483 infoText += "No data\n" 4484 4485 return infoText 4486 4487 def OverviewAccounts(self, show: bool = False) -> dict: 4488 """ 4489 Method for parsing and show simple table with all available user accounts. 4490 4491 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4492 4493 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4494 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4495 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4496 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4497 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4498 "closed": "—", "access": "Full access" }, ...}}` 4499 """ 4500 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4501 4502 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4503 accounts = { 4504 item["id"]: { 4505 "type": TKS_ACCOUNT_TYPES[item["type"]], 4506 "name": item["name"], 4507 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4508 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4509 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4510 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4511 } for item in rawAccounts["accounts"] 4512 } 4513 4514 # Raw and parsed data with some fields replaced in "stat" section: 4515 view = { 4516 "rawAccounts": rawAccounts, 4517 "stat": accounts, 4518 } 4519 4520 # --- Prepare simple text table with only accounts data in human-readable format: 4521 if show: 4522 info = [ 4523 "# User accounts\n\n", 4524 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4525 "| Account ID | Type | Status | Name |\n", 4526 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4527 ] 4528 4529 for account in view["stat"].keys(): 4530 info.extend([ 4531 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4532 account, 4533 view["stat"][account]["type"], 4534 view["stat"][account]["status"], 4535 view["stat"][account]["name"], 4536 ) 4537 ]) 4538 4539 infoText = "".join(info) 4540 4541 uLogger.info(infoText) 4542 4543 if self.userAccountsFile: 4544 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4545 fH.write(infoText) 4546 4547 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4548 4549 if self.useHTMLReports: 4550 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4551 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4552 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4553 4554 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4555 4556 return view 4557 4558 def OverviewUserInfo(self, show: bool = False) -> dict: 4559 """ 4560 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4561 4562 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4563 4564 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4565 :return: dict with raw parsed data from server and some calculated statistics about it. 4566 """ 4567 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4568 tmpTicker = self._ticker 4569 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4570 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4571 self._ticker = tmpTicker 4572 4573 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4574 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4575 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4576 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4577 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4578 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4579 4580 # This is dict with parsed common user data: 4581 userInfo = { 4582 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4583 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4584 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4585 "tariff": rawUserInfo["tariff"], 4586 } 4587 4588 # This is an array of dict with parsed margin statuses for every account IDs: 4589 margins = {} 4590 for accountId in accounts.keys(): 4591 if rawMargins[accountId]: 4592 margins[accountId] = { 4593 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4594 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4595 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4596 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4597 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4598 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4599 "missing": missing["volume"], 4600 } 4601 4602 else: 4603 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4604 4605 unary = {} # unary-connection limits 4606 for item in rawTariffLimits["unaryLimits"]: 4607 if item["limitPerMinute"] in unary.keys(): 4608 unary[item["limitPerMinute"]].extend(item["methods"]) 4609 4610 else: 4611 unary[item["limitPerMinute"]] = item["methods"] 4612 4613 stream = {} # stream-connection limits 4614 for item in rawTariffLimits["streamLimits"]: 4615 if item["limit"] in stream.keys(): 4616 stream[item["limit"]].extend(item["streams"]) 4617 4618 else: 4619 stream[item["limit"]] = item["streams"] 4620 4621 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4622 limits = { 4623 "unary": unary, 4624 "stream": stream, 4625 } 4626 4627 # Raw and parsed data as an output result: 4628 view = { 4629 "rawUserInfo": rawUserInfo, 4630 "rawAccounts": rawAccounts, 4631 "rawMargins": rawMargins, 4632 "rawTariffLimits": rawTariffLimits, 4633 "stat": { 4634 "overview": overview, 4635 "userInfo": userInfo, 4636 "accounts": accounts, 4637 "margins": margins, 4638 "limits": limits, 4639 }, 4640 } 4641 4642 # --- Prepare text table with user information in human-readable format: 4643 if show: 4644 info = [ 4645 "# Full user information\n\n", 4646 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4647 "## Common information\n\n", 4648 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4649 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4650 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4651 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4652 "\n## User accounts\n\n", 4653 ] 4654 4655 for account in view["stat"]["accounts"].keys(): 4656 info.extend([ 4657 "### ID: [{}]\n\n".format(account), 4658 "| Parameters | Values |\n", 4659 "|----------------------|--------------------------------------------------------------|\n", 4660 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4661 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4662 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4663 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4664 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4665 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4666 ]) 4667 4668 if margins[account]: 4669 info.extend([ 4670 "| Margin status: | Enabled |\n", 4671 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4672 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4673 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4674 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4675 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4676 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4677 ]) 4678 4679 else: 4680 info.append("| Margin status: | Disabled |\n\n") 4681 4682 info.extend([ 4683 "\n## Current user tariff limits\n", 4684 "\n### See also\n", 4685 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4686 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4687 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4688 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4689 "\n### Unary limits\n", 4690 ]) 4691 4692 if unary: 4693 for key, values in sorted(unary.items()): 4694 info.append("\n* Max requests per minute: {}\n".format(key)) 4695 4696 for value in values: 4697 info.append(" - {}\n".format(value)) 4698 4699 else: 4700 info.append("\nNot available\n") 4701 4702 info.append("\n### Stream limits\n") 4703 4704 if stream: 4705 for key, values in sorted(stream.items()): 4706 info.append("\n* Max stream connections: {}\n".format(key)) 4707 4708 for value in values: 4709 info.append(" - {}\n".format(value)) 4710 4711 else: 4712 info.append("\nNot available\n") 4713 4714 infoText = "".join(info) 4715 4716 uLogger.info(infoText) 4717 4718 if self.userInfoFile: 4719 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4720 fH.write(infoText) 4721 4722 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4723 4724 if self.useHTMLReports: 4725 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4726 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4727 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4728 4729 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4730 4731 return view 4732 4733 4734class Args: 4735 """ 4736 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4737 """ 4738 def __init__(self, **kwargs): 4739 self.__dict__.update(kwargs) 4740 4741 def __getattr__(self, item): 4742 return None 4743 4744 4745def ParseArgs(): 4746 """This function get and parse command line keys.""" 4747 parser = ArgumentParser() # command-line string parser 4748 4749 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4750 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4751 4752 # --- options: 4753 4754 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4755 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4756 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4757 4758 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4759 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4760 4761 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4762 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4763 4764 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4765 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4766 4767 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4768 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4769 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4770 4771 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4772 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4773 4774 # --- commands: 4775 4776 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4777 4778 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4779 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4780 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4781 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4782 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4783 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4784 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4785 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4786 4787 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4788 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4789 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4790 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4791 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4792 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4793 4794 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4795 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4796 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4797 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4798 4799 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4800 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4801 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4802 4803 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4804 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4805 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4806 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4807 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4808 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4809 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4810 4811 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4812 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4813 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4814 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4815 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4816 4817 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4818 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4819 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4820 4821 cmdArgs = parser.parse_args() 4822 return cmdArgs 4823 4824 4825def Main(**kwargs): 4826 """ 4827 Main function for work with TKSBrokerAPI in the console. 4828 4829 See examples: 4830 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4831 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4832 """ 4833 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4834 4835 if args.debug_level: 4836 uLogger.level = 10 # always debug level by default 4837 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4838 4839 exitCode = 0 4840 start = datetime.now(tzutc()) 4841 uLogger.debug("=-" * 50) 4842 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4843 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4844 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4845 )) 4846 4847 # trying to calculate full current version: 4848 buildVersion = __version__ 4849 try: 4850 v = version("tksbrokerapi") 4851 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4852 4853 except Exception: 4854 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4855 4856 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4857 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4858 4859 try: 4860 if args.version: 4861 print("TKSBrokerAPI {}".format(buildVersion)) 4862 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4863 4864 else: 4865 # Init class for trading with Tinkoff Broker: 4866 trader = TinkoffBrokerServer( 4867 token=args.token, 4868 accountId=args.account_id, 4869 useCache=not args.no_cache, 4870 ) 4871 4872 # --- set some options: 4873 4874 if args.more: 4875 trader.moreDebug = True 4876 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4877 4878 if args.html: 4879 trader.useHTMLReports = True 4880 4881 if args.ticker: 4882 ticker = str(args.ticker).upper() # Tickers may be upper case only 4883 4884 if ticker in trader.aliasesKeys: 4885 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4886 4887 else: 4888 trader.ticker = ticker 4889 4890 if args.figi: 4891 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4892 4893 if args.depth is not None: 4894 trader.depth = args.depth 4895 4896 # --- do one command: 4897 4898 if args.list: 4899 if args.output is not None: 4900 trader.instrumentsFile = args.output 4901 4902 trader.ShowInstrumentsInfo(show=True) 4903 4904 elif args.list_xlsx: 4905 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4906 4907 elif args.bonds_xlsx is not None: 4908 if args.output is not None: 4909 trader.bondsXLSXFile = args.output 4910 4911 if len(args.bonds_xlsx) == 0: 4912 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4913 4914 else: 4915 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4916 4917 elif args.search: 4918 if args.output is not None: 4919 trader.searchResultsFile = args.output 4920 4921 trader.SearchInstruments(pattern=args.search[0], show=True) 4922 4923 elif args.info: 4924 if not (args.ticker or args.figi): 4925 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4926 raise Exception("Ticker or FIGI required") 4927 4928 if args.output is not None: 4929 trader.infoFile = args.output 4930 4931 if args.ticker: 4932 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4933 4934 else: 4935 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4936 4937 elif args.calendar is not None: 4938 if args.output is not None: 4939 trader.calendarFile = args.output 4940 4941 if len(args.calendar) == 0: 4942 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4943 4944 else: 4945 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4946 4947 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4948 4949 elif args.price: 4950 if not (args.ticker or args.figi): 4951 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4952 raise Exception("Ticker or FIGI required") 4953 4954 trader.GetCurrentPrices(show=True) 4955 4956 elif args.prices is not None: 4957 if args.output is not None: 4958 trader.pricesFile = args.output 4959 4960 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4961 4962 elif args.overview: 4963 if args.output is not None: 4964 trader.overviewFile = args.output 4965 4966 trader.Overview(show=True, details="full") 4967 4968 elif args.overview_digest: 4969 if args.output is not None: 4970 trader.overviewDigestFile = args.output 4971 4972 trader.Overview(show=True, details="digest") 4973 4974 elif args.overview_positions: 4975 if args.output is not None: 4976 trader.overviewPositionsFile = args.output 4977 4978 trader.Overview(show=True, details="positions") 4979 4980 elif args.overview_orders: 4981 if args.output is not None: 4982 trader.overviewOrdersFile = args.output 4983 4984 trader.Overview(show=True, details="orders") 4985 4986 elif args.overview_analytics: 4987 if args.output is not None: 4988 trader.overviewAnalyticsFile = args.output 4989 4990 trader.Overview(show=True, details="analytics") 4991 4992 elif args.overview_calendar: 4993 if args.output is not None: 4994 trader.overviewAnalyticsFile = args.output 4995 4996 trader.Overview(show=True, details="calendar") 4997 4998 elif args.deals is not None: 4999 if args.output is not None: 5000 trader.reportFile = args.output 5001 5002 if 0 <= len(args.deals) < 3: 5003 trader.Deals( 5004 start=args.deals[0] if len(args.deals) >= 1 else None, 5005 end=args.deals[1] if len(args.deals) == 2 else None, 5006 show=True, # Always show deals report in console 5007 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5008 ) 5009 5010 else: 5011 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5012 raise Exception("Incorrect value") 5013 5014 elif args.history is not None: 5015 if args.output is not None: 5016 trader.historyFile = args.output 5017 5018 if 0 <= len(args.history) < 3: 5019 dataReceived = trader.History( 5020 start=args.history[0] if len(args.history) >= 1 else None, 5021 end=args.history[1] if len(args.history) == 2 else None, 5022 interval="hour" if args.interval is None or not args.interval else args.interval, 5023 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5024 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5025 show=True, # shows all downloaded candles in console 5026 ) 5027 5028 if args.render_chart is not None and dataReceived is not None: 5029 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5030 5031 trader.ShowHistoryChart( 5032 candles=dataReceived, 5033 interact=iChart, 5034 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5035 ) 5036 5037 else: 5038 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5039 raise Exception("Incorrect value") 5040 5041 elif args.load_history is not None: 5042 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5043 5044 if args.render_chart is not None and histData is not None: 5045 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5046 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5047 5048 trader.ShowHistoryChart( 5049 candles=histData, 5050 interact=iChart, 5051 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5052 ) 5053 5054 elif args.trade is not None: 5055 if 1 <= len(args.trade) <= 5: 5056 trader.Trade( 5057 operation=args.trade[0], 5058 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5059 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5060 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5061 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5062 ) 5063 5064 else: 5065 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5066 5067 elif args.buy is not None: 5068 if 0 <= len(args.buy) <= 4: 5069 trader.Buy( 5070 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5071 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5072 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5073 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5074 ) 5075 5076 else: 5077 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5078 5079 elif args.sell is not None: 5080 if 0 <= len(args.sell) <= 4: 5081 trader.Sell( 5082 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5083 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5084 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5085 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5086 ) 5087 5088 else: 5089 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5090 5091 elif args.order: 5092 if 4 <= len(args.order) <= 7: 5093 trader.Order( 5094 operation=args.order[0], 5095 orderType=args.order[1], 5096 lots=int(args.order[2]), 5097 targetPrice=float(args.order[3]), 5098 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5099 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5100 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5101 ) 5102 5103 else: 5104 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5105 5106 elif args.buy_limit: 5107 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5108 5109 elif args.sell_limit: 5110 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5111 5112 elif args.buy_stop: 5113 if 2 <= len(args.buy_stop) <= 7: 5114 trader.BuyStop( 5115 lots=int(args.buy_stop[0]), 5116 targetPrice=float(args.buy_stop[1]), 5117 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5118 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5119 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5120 ) 5121 5122 else: 5123 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5124 5125 elif args.sell_stop: 5126 if 2 <= len(args.sell_stop) <= 7: 5127 trader.SellStop( 5128 lots=int(args.sell_stop[0]), 5129 targetPrice=float(args.sell_stop[1]), 5130 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5131 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5132 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5133 ) 5134 5135 else: 5136 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5137 5138 # elif args.buy_order_grid is not None: 5139 # # update order grid work with api v2 5140 # if len(args.buy_order_grid) == 2: 5141 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5142 # 5143 # for order in orderParams: 5144 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5145 # 5146 # else: 5147 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5148 # 5149 # elif args.sell_order_grid is not None: 5150 # # update order grid work with api v2 5151 # if len(args.sell_order_grid) >= 2: 5152 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5153 # 5154 # for order in orderParams: 5155 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5156 # 5157 # else: 5158 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5159 5160 elif args.close_order is not None: 5161 trader.CloseOrders(args.close_order) # close only one order 5162 5163 elif args.close_orders is not None: 5164 trader.CloseOrders(args.close_orders) # close list of orders 5165 5166 elif args.close_trade: 5167 if not (args.ticker or args.figi): 5168 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5169 raise Exception("Ticker or FIGI required") 5170 5171 if args.ticker: 5172 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5173 5174 else: 5175 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5176 5177 elif args.close_trades is not None: 5178 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5179 5180 elif args.close_all is not None: 5181 if args.ticker: 5182 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5183 5184 elif args.figi: 5185 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5186 5187 else: 5188 trader.CloseAll(*args.close_all) 5189 5190 elif args.limits: 5191 if args.output is not None: 5192 trader.withdrawalLimitsFile = args.output 5193 5194 trader.OverviewLimits(show=True) 5195 5196 elif args.user_info: 5197 if args.output is not None: 5198 trader.userInfoFile = args.output 5199 5200 trader.OverviewUserInfo(show=True) 5201 5202 elif args.account: 5203 if args.output is not None: 5204 trader.userAccountsFile = args.output 5205 5206 trader.OverviewAccounts(show=True) 5207 5208 else: 5209 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5210 raise Exception("There is no command to execute") 5211 5212 except Exception: 5213 trace = tb.format_exc() 5214 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5215 if e in trace: 5216 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5217 break 5218 5219 uLogger.debug(trace) 5220 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5221 exitCode = 255 # an error occurred, must be open a ticket for this issue 5222 5223 finally: 5224 finish = datetime.now(tzutc()) 5225 5226 if exitCode == 0: 5227 if args.more: 5228 uLogger.debug("All operations were finished success (summary code is 0).") 5229 5230 else: 5231 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5232 os.path.abspath(uLog.defaultLogFile), exitCode, 5233 )) 5234 5235 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5236 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5237 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5238 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5239 )) 5240 uLogger.debug("=-" * 50) 5241 5242 if not kwargs: 5243 sys.exit(exitCode) 5244 5245 else: 5246 return exitCode 5247 5248 5249if __name__ == "__main__": 5250 Main()
78class TinkoffBrokerServer: 79 """ 80 This class implements methods to work with Tinkoff broker server. 81 82 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 83 84 About `token`: https://tinkoff.github.io/investAPI/token/ 85 """ 86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self.__lock = Lock() # initialize multiprocessing mutex lock 130 131 self.aliases = TKS_TICKER_ALIASES 132 """Some aliases instead official tickers. 133 134 See also: `TKSEnums.TKS_TICKER_ALIASES` 135 """ 136 137 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 138 139 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 140 141 self._ticker = "" 142 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 143 144 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 145 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 146 147 See also: `SearchByTicker()`, `SearchInstruments()`. 148 """ 149 150 self._figi = "" 151 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 152 153 See also: `SearchByFIGI()`, `SearchInstruments()`. 154 """ 155 156 self.depth = 1 157 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 158 159 See also: `GetCurrentPrices()`. 160 """ 161 162 self.server = r"https://invest-public-api.tinkoff.ru/rest" 163 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 164 165 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 166 """ 167 168 uLogger.debug("Broker API server: {}".format(self.server)) 169 170 self.timeout = 15 171 """Server operations timeout in seconds. Default: `15`. 172 173 See also: `SendAPIRequest()`. 174 """ 175 176 self.headers = { 177 "Content-Type": "application/json", 178 "accept": "application/json", 179 "Authorization": "Bearer {}".format(self.token), 180 "x-app-name": "Tim55667757.TKSBrokerAPI", 181 } 182 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 183 184 See also: `SendAPIRequest()`. 185 """ 186 187 self.body = None 188 """Request body which send to broker server. Default: `None`. 189 190 See also: `SendAPIRequest()`. 191 """ 192 193 self.moreDebug = False 194 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 195 196 self.useHTMLReports = False 197 """ 198 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 199 200 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 201 """ 202 203 self.historyFile = None 204 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 205 206 See also: `History()`. 207 """ 208 209 self.htmlHistoryFile = "index.html" 210 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 211 212 See also: `ShowHistoryChart()`. 213 """ 214 215 self.instrumentsFile = "instruments.md" 216 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 217 218 See also: `ShowInstrumentsInfo()`. 219 """ 220 221 self.searchResultsFile = "search-results.md" 222 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 223 224 See also: `SearchInstruments()`. 225 """ 226 227 self.pricesFile = "prices.md" 228 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 229 230 See also: `GetListOfPrices()`. 231 """ 232 233 self.infoFile = "info.md" 234 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 235 236 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 237 """ 238 239 self.bondsXLSXFile = "ext-bonds.xlsx" 240 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 241 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 242 243 See also: `ExtendBondsData()`. 244 """ 245 246 self.calendarFile = "calendar.md" 247 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 248 249 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 250 251 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 252 """ 253 254 self.overviewFile = "overview.md" 255 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 256 257 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 258 """ 259 260 self.overviewDigestFile = "overview-digest.md" 261 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 262 263 See also: `Overview()` with parameter `details="digest"`. 264 """ 265 266 self.overviewPositionsFile = "overview-positions.md" 267 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 268 269 See also: `Overview()` with parameter `details="positions"`. 270 """ 271 272 self.overviewOrdersFile = "overview-orders.md" 273 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 274 275 See also: `Overview()` with parameter `details="orders"`. 276 """ 277 278 self.overviewAnalyticsFile = "overview-analytics.md" 279 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 280 281 See also: `Overview()` with parameter `details="analytics"`. 282 """ 283 284 self.overviewBondsCalendarFile = "overview-calendar.md" 285 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 286 287 See also: `Overview()` with parameter `details="calendar"`. 288 """ 289 290 self.reportFile = "deals.md" 291 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 292 293 See also: `Deals()`. 294 """ 295 296 self.withdrawalLimitsFile = "limits.md" 297 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 298 299 See also: `OverviewLimits()` and `RequestLimits()`. 300 """ 301 302 self.userInfoFile = "user-info.md" 303 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 304 305 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 306 """ 307 308 self.userAccountsFile = "accounts.md" 309 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 310 311 See also: `OverviewAccounts()`, `RequestAccounts()`. 312 """ 313 314 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 315 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 316 317 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 318 319 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 320 """ 321 322 self.iList = None # init iList for raw instruments data 323 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 324 325 See also: `Listing()`, `DumpInstruments()`. 326 """ 327 328 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 329 if useCache: 330 if os.path.exists(self.iListDumpFile): 331 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 332 curTime = datetime.now(tzutc()) 333 334 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 335 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 336 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 338 339 else: 340 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 341 342 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 343 os.path.abspath(self.iListDumpFile), 344 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 345 )) 346 347 else: 348 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 349 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 350 351 else: 352 self.iList = self.Listing() # request new raw instruments data from broker server 353 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 354 355 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 356 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 357 358 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 359 """ 360 361 @property 362 def ticker(self) -> str: 363 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 364 365 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 366 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 367 368 See also: `SearchByTicker()`, `SearchInstruments()`. 369 """ 370 return self._ticker 371 372 @ticker.setter 373 def ticker(self, value): 374 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 375 376 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 377 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 378 379 See also: `SearchByTicker()`, `SearchInstruments()`. 380 """ 381 self._ticker = str(value).upper() # Tickers may be upper case only 382 383 @property 384 def figi(self) -> str: 385 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 386 387 See also: `SearchByFIGI()`, `SearchInstruments()`. 388 """ 389 return self._figi 390 391 @figi.setter 392 def figi(self, value): 393 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 394 395 See also: `SearchByFIGI()`, `SearchInstruments()`. 396 """ 397 self._figi = str(value).upper() # FIGI may be upper case only 398 399 def _ParseJSON(self, rawData="{}") -> dict: 400 """ 401 Parse JSON from response string. 402 403 :param rawData: this is a string with JSON-formatted text. 404 :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`. 405 """ 406 try: 407 responseJSON = json.loads(rawData) if rawData else {} 408 409 if self.moreDebug: 410 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 411 412 return responseJSON 413 414 except Exception as e: 415 uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e)) 416 return {} 417 418 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 419 """ 420 Send GET or POST request to broker server and receive JSON object. 421 422 self.header: must be defining with dictionary of headers. 423 self.body: if define then used as request body. None by default. 424 self.timeout: global request timeout, 15 seconds by default. 425 :param url: url with REST request. 426 :param reqType: send "GET" or "POST" request. "GET" by default. 427 :param retry: how many times retry after first request if an 5xx server errors occurred. 428 :param pause: sleep time in seconds between retries. 429 :return: response JSON (dictionary) from broker. 430 """ 431 if reqType.upper() not in ("GET", "POST"): 432 uLogger.error("You can define request type: `GET` or `POST`!") 433 raise Exception("Incorrect value") 434 435 if self.moreDebug: 436 uLogger.debug("Request parameters:") 437 uLogger.debug(" - REST API URL: {}".format(url)) 438 uLogger.debug(" - request type: {}".format(reqType)) 439 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 440 uLogger.debug(" - body:\n{}".format(self.body)) 441 442 # fast hack to avoid all operations with some tickers/FIGI 443 responseJSON = {} 444 oK = True 445 for item in self.exclude: 446 if item in url: 447 if self.moreDebug: 448 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 449 450 oK = False 451 break 452 453 if oK: 454 with self.__lock: # acquire the mutex lock 455 counter = 0 456 response = None 457 errMsg = "" 458 459 while not response and counter <= retry: 460 if reqType == "GET": 461 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 462 463 if reqType == "POST": 464 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 465 466 if self.moreDebug: 467 uLogger.debug("Response:") 468 uLogger.debug(" - status code: {}".format(response.status_code)) 469 uLogger.debug(" - reason: {}".format(response.reason)) 470 uLogger.debug(" - body length: {}".format(len(response.text))) 471 uLogger.debug(" - headers:\n{}".format(response.headers)) 472 473 # Server returns some headers: 474 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 475 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 476 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 477 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 478 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 479 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 480 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 481 sleep(rateLimitWait) 482 483 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 484 if 400 <= response.status_code < 500: 485 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 486 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 487 488 if "code" in response.text and "message" in response.text: 489 msgDict = self._ParseJSON(rawData=response.text) 490 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 491 492 counter = retry + 1 # do not retry for 4xx errors 493 494 if 500 <= response.status_code < 600: 495 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 496 uLogger.debug(" - not oK, {}".format(errMsg)) 497 498 if "code" in response.text and "message" in response.text: 499 errMsgDict = self._ParseJSON(rawData=response.text) 500 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 501 502 counter += 1 503 504 if counter <= retry: 505 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 506 sleep(pause) 507 508 responseJSON = self._ParseJSON(rawData=response.text) 509 510 if errMsg: 511 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 512 uLogger.error(" - not oK, {}".format(errMsg)) 513 514 return responseJSON 515 516 def _IUpdater(self, iType: str) -> tuple: 517 """ 518 Request instrument by type from server. See available API methods for instruments: 519 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 520 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 521 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 522 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 523 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 524 525 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 526 :return: tuple with iType name and list of available instruments of current type for defined user token. 527 """ 528 result = [] 529 530 if iType in TKS_INSTRUMENTS: 531 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 532 533 # all instruments have the same body in API v2 requests: 534 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 535 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 536 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 537 538 return iType, result 539 540 def _IWrapper(self, kwargs): 541 """ 542 Wrapper runs instrument's update method `_IUpdater()`. 543 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 544 """ 545 return self._IUpdater(**kwargs) 546 547 def Listing(self) -> dict: 548 """ 549 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 550 551 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 552 """ 553 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 554 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 555 556 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 557 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 558 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 559 560 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 561 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 562 poolUpdater.close() # close the thread pool 563 poolUpdater.join() # wait a moment until all data returns from threads 564 565 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 566 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 567 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 568 569 # calculate minimum price increment (step) for all instruments and set up instrument's type: 570 for iType in iList.keys(): 571 for ticker in iList[iType]: 572 iList[iType][ticker]["type"] = iType 573 574 if "minPriceIncrement" in iList[iType][ticker].keys(): 575 iList[iType][ticker]["step"] = NanoToFloat( 576 iList[iType][ticker]["minPriceIncrement"]["units"], 577 iList[iType][ticker]["minPriceIncrement"]["nano"], 578 ) 579 580 else: 581 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 582 583 return iList 584 585 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 586 """ 587 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 588 589 See also: `DumpInstruments()`, `Listing()`. 590 591 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 592 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 593 """ 594 if self.iListDumpFile is None or not self.iListDumpFile: 595 uLogger.error("Output name of dump file must be defined!") 596 raise Exception("Filename required") 597 598 if not self.iList or forceUpdate: 599 self.iList = self.Listing() 600 601 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 602 603 # Save as XLSX with separated sheets for every type of instruments: 604 with pd.ExcelWriter( 605 path=xlsxDumpFile, 606 date_format=TKS_DATE_FORMAT, 607 datetime_format=TKS_DATE_TIME_FORMAT, 608 mode="w", 609 ) as writer: 610 for iType in TKS_INSTRUMENTS: 611 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 612 df = df[sorted(df)] # sorted by column names 613 df = df.applymap( 614 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 615 na_action="ignore", 616 ) # converting numbers from nano-type to float in every cell 617 df.to_excel( 618 writer, 619 sheet_name=iType, 620 encoding="UTF-8", 621 freeze_panes=(1, 1), 622 ) # saving as XLSX-file with freeze first row and column as headers 623 624 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 625 626 def DumpInstruments(self, forceUpdate: bool = True) -> str: 627 """ 628 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 629 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 630 631 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 632 633 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 634 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 635 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 636 """ 637 if self.iListDumpFile is None or not self.iListDumpFile: 638 uLogger.error("Output name of dump file must be defined!") 639 raise Exception("Filename required") 640 641 if not self.iList or forceUpdate: 642 self.iList = self.Listing() 643 644 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 645 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 646 fH.write(jsonDump) 647 648 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 649 650 return jsonDump 651 652 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 653 """ 654 Show information about one instrument defined by json data and prints it in Markdown format. 655 656 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 657 658 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 659 :param show: if `True` then also printing information about instrument and its current price. 660 :return: multilines text in Markdown format with information about one instrument. 661 """ 662 splitLine = "| | |\n" 663 infoText = "" 664 665 if iJSON is not None and iJSON and isinstance(iJSON, dict): 666 info = [ 667 "# Main information\n\n", 668 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 669 "| Parameters | Values |\n", 670 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 671 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 672 "| Full name: | {:<54} |\n".format(iJSON["name"]), 673 ] 674 675 if "sector" in iJSON.keys() and iJSON["sector"]: 676 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 677 678 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 679 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 680 681 info.extend([ 682 splitLine, 683 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 684 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 685 ]) 686 687 if "isin" in iJSON.keys() and iJSON["isin"]: 688 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 689 690 if "classCode" in iJSON.keys(): 691 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 692 693 info.extend([ 694 splitLine, 695 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 696 splitLine, 697 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 698 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 699 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 700 ]) 701 702 if iJSON["figi"]: 703 self._figi = iJSON["figi"] 704 iJSON = iJSON | self.RequestTradingStatus() 705 706 info.extend([ 707 splitLine, 708 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 709 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 710 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 711 ]) 712 713 info.append(splitLine) 714 715 if "type" in iJSON.keys() and iJSON["type"]: 716 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 717 718 if "shareType" in iJSON.keys() and iJSON["shareType"]: 719 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 720 721 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 722 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 723 724 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 725 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 726 727 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 728 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 729 730 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 731 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 732 733 if "focusType" in iJSON.keys() and iJSON["focusType"]: 734 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 735 736 if "assetType" in iJSON.keys() and iJSON["assetType"]: 737 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 738 739 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 740 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 741 742 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 743 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 744 745 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 746 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 747 748 if "currency" in iJSON.keys(): 749 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 750 751 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 752 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 753 754 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 755 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 756 757 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 758 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 759 760 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 761 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 762 763 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 764 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 765 766 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 767 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 768 769 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 770 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 771 772 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 773 info.append("| Perpetual bond: | Yes |\n") 774 775 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 776 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 777 778 iExt = None 779 if iJSON["type"] == "Bonds": 780 info.extend([ 781 splitLine, 782 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 783 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 784 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 785 iJSON["nominal"]["currency"], 786 )), 787 ]) 788 789 if "floatingCouponFlag" in iJSON.keys(): 790 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 791 792 if "amortizationFlag" in iJSON.keys(): 793 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 794 795 info.append(splitLine) 796 797 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 798 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 799 800 if iJSON["figi"]: 801 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 802 803 info.extend([ 804 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 805 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 806 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 807 ]) 808 809 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 810 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 811 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 812 iJSON["aciValue"]["currency"] 813 ))) 814 815 if "currentPrice" in iJSON.keys(): 816 info.append(splitLine) 817 818 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 819 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 820 821 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 822 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 823 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 824 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 825 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 826 827 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 828 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 829 830 info.extend([ 831 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 832 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 833 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 834 )), 835 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 836 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 837 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 838 )), 839 "| Changes between last deal price and last close | {:<54} |\n".format( 840 "{:.2f}%{}".format( 841 iJSON["currentPrice"]["changes"], 842 " ({}{:.2f} {})".format( 843 "+" if bondChangesDelta > 0 else "", 844 bondChangesDelta, 845 aciCurrency 846 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 847 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 848 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 849 currency 850 ), 851 ) 852 ), 853 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 854 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 855 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 856 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 857 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 858 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 859 )), 860 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 861 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 862 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 863 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 864 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 865 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 866 )), 867 ]) 868 869 if "lot" in iJSON.keys(): 870 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 871 872 if "step" in iJSON.keys() and iJSON["step"] != 0: 873 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 874 875 # Add bond payment calendar: 876 if iJSON["type"] == "Bonds": 877 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 878 info.extend(["\n#", strCalendar]) 879 880 infoText += "".join(info) 881 882 if show: 883 uLogger.info("{}".format(infoText)) 884 885 else: 886 uLogger.debug("{}".format(infoText)) 887 888 if self.infoFile is not None: 889 with open(self.infoFile, "w", encoding="UTF-8") as fH: 890 fH.write(infoText) 891 892 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 893 894 if self.useHTMLReports: 895 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 896 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 897 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 898 899 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 900 901 return infoText 902 903 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 904 """ 905 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 906 907 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 908 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 909 :return: JSON formatted data with information about instrument. 910 """ 911 tickerJSON = {} 912 if self.moreDebug: 913 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 914 915 if not self._ticker: 916 uLogger.warning("self._ticker variable is not be empty!") 917 918 else: 919 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 920 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 921 raise Exception("Instrument not allowed") 922 923 if not self.iList: 924 self.iList = self.Listing() 925 926 if self._ticker in self.iList["Shares"].keys(): 927 tickerJSON = self.iList["Shares"][self._ticker] 928 if self.moreDebug: 929 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 930 931 elif self._ticker in self.iList["Currencies"].keys(): 932 tickerJSON = self.iList["Currencies"][self._ticker] 933 if self.moreDebug: 934 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 935 936 elif self._ticker in self.iList["Bonds"].keys(): 937 tickerJSON = self.iList["Bonds"][self._ticker] 938 if self.moreDebug: 939 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 940 941 elif self._ticker in self.iList["Etfs"].keys(): 942 tickerJSON = self.iList["Etfs"][self._ticker] 943 if self.moreDebug: 944 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 945 946 elif self._ticker in self.iList["Futures"].keys(): 947 tickerJSON = self.iList["Futures"][self._ticker] 948 if self.moreDebug: 949 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 950 951 if tickerJSON: 952 self._figi = tickerJSON["figi"] 953 954 if requestPrice: 955 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 956 957 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 958 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 959 960 else: 961 tickerJSON["currentPrice"]["changes"] = 0 962 963 if show: 964 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 965 966 else: 967 if show: 968 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 969 970 return tickerJSON 971 972 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 973 """ 974 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 975 976 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 977 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 978 :return: JSON formatted data with information about instrument. 979 """ 980 figiJSON = {} 981 if self.moreDebug: 982 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 983 984 if not self._figi: 985 uLogger.warning("self._figi variable is not be empty!") 986 987 else: 988 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 989 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 990 raise Exception("Instrument not allowed") 991 992 if not self.iList: 993 self.iList = self.Listing() 994 995 for item in self.iList["Shares"].keys(): 996 if self._figi == self.iList["Shares"][item]["figi"]: 997 figiJSON = self.iList["Shares"][item] 998 999 if self.moreDebug: 1000 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1001 1002 break 1003 1004 if not figiJSON: 1005 for item in self.iList["Currencies"].keys(): 1006 if self._figi == self.iList["Currencies"][item]["figi"]: 1007 figiJSON = self.iList["Currencies"][item] 1008 1009 if self.moreDebug: 1010 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1011 1012 break 1013 1014 if not figiJSON: 1015 for item in self.iList["Bonds"].keys(): 1016 if self._figi == self.iList["Bonds"][item]["figi"]: 1017 figiJSON = self.iList["Bonds"][item] 1018 1019 if self.moreDebug: 1020 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1021 1022 break 1023 1024 if not figiJSON: 1025 for item in self.iList["Etfs"].keys(): 1026 if self._figi == self.iList["Etfs"][item]["figi"]: 1027 figiJSON = self.iList["Etfs"][item] 1028 1029 if self.moreDebug: 1030 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1031 1032 break 1033 1034 if not figiJSON: 1035 for item in self.iList["Futures"].keys(): 1036 if self._figi == self.iList["Futures"][item]["figi"]: 1037 figiJSON = self.iList["Futures"][item] 1038 1039 if self.moreDebug: 1040 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1041 1042 break 1043 1044 if figiJSON: 1045 self._figi = figiJSON["figi"] 1046 self._ticker = figiJSON["ticker"] 1047 1048 if requestPrice: 1049 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1050 1051 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1052 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1053 1054 else: 1055 figiJSON["currentPrice"]["changes"] = 0 1056 1057 if show: 1058 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1059 1060 else: 1061 if show: 1062 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1063 1064 return figiJSON 1065 1066 def GetCurrentPrices(self, show: bool = True) -> dict: 1067 """ 1068 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1069 `{"buy": [{"price": 1243.8, "quantity": 193}, 1070 {"price": 1244.0, "quantity": 168}, 1071 {"price": 1244.8, "quantity": 5}, 1072 {"price": 1245.0, "quantity": 61}, 1073 {"price": 1245.4, "quantity": 60}], 1074 "sell": [{"price": 1243.6, "quantity": 8}, 1075 {"price": 1242.6, "quantity": 10}, 1076 {"price": 1242.4, "quantity": 18}, 1077 {"price": 1242.2, "quantity": 50}, 1078 {"price": 1242.0, "quantity": 113}], 1079 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1080 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1081 - sell: list of dicts with Buyers prices, 1082 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1083 - quantity: volume value by current price in lots, 1084 - limitUp: current trade session limit price, maximum, 1085 - limitDown: current trade session limit price, minimum, 1086 - lastPrice: last deal price of the instrument, 1087 - closePrice: previous trade session close price of the instrument. 1088 1089 See also: `SearchByTicker()` and `SearchByFIGI()`. 1090 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1091 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1092 1093 :param show: if `True` then print DOM to log and console. 1094 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1095 If an error occurred then returns an empty record: 1096 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1097 """ 1098 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1099 1100 if self.depth < 1: 1101 uLogger.error("Depth of Market (DOM) must be >=1!") 1102 raise Exception("Incorrect value") 1103 1104 if not (self._ticker or self._figi): 1105 uLogger.error("self._ticker or self._figi variables must be defined!") 1106 raise Exception("Ticker or FIGI required") 1107 1108 if self._ticker and not self._figi: 1109 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1110 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1111 1112 if not self._ticker and self._figi: 1113 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1114 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1115 1116 if not self._figi: 1117 uLogger.error("FIGI is not defined!") 1118 raise Exception("Ticker or FIGI required") 1119 1120 else: 1121 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1122 1123 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1124 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1125 self.body = str({"figi": self._figi, "depth": self.depth}) 1126 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1127 1128 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1129 # list of dicts with sellers orders: 1130 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1131 1132 # list of dicts with buyers orders: 1133 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1134 1135 # max price of instrument at this time: 1136 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1137 1138 # min price of instrument at this time: 1139 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1140 1141 # last price of deal with instrument: 1142 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1143 1144 # last close price of instrument: 1145 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1146 1147 else: 1148 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1149 uLogger.debug("Server response: {}".format(pricesResponse)) 1150 1151 if show: 1152 if prices["buy"] or prices["sell"]: 1153 info = [ 1154 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1155 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1156 self._ticker, 1157 self._figi, 1158 self.depth, 1159 ), 1160 "-" * 60, "\n", 1161 " Orders of Buyers | Orders of Sellers\n", 1162 "-" * 60, "\n", 1163 " Sell prices (volumes) | Buy prices (volumes)\n", 1164 "-" * 60, "\n", 1165 ] 1166 1167 if not prices["buy"]: 1168 info.append(" | No orders!\n") 1169 sumBuy = 0 1170 1171 else: 1172 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1173 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1174 for item in maxMinSorted: 1175 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1176 1177 if not prices["sell"]: 1178 info.append("No orders! |\n") 1179 sumSell = 0 1180 1181 else: 1182 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1183 for item in prices["sell"]: 1184 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1185 1186 info.extend([ 1187 "-" * 60, "\n", 1188 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1189 "-" * 60, "\n", 1190 ]) 1191 1192 infoText = "".join(info) 1193 1194 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1195 1196 else: 1197 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1198 1199 return prices 1200 1201 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1202 """ 1203 This method get and show information about all available broker instruments for current user account. 1204 If `instrumentsFile` string is not empty then also save information to this file. 1205 1206 :param show: if `True` then print results to console, if `False` — print only to file. 1207 :return: multi-lines string with all available broker instruments 1208 """ 1209 if not self.iList: 1210 self.iList = self.Listing() 1211 1212 info = [ 1213 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1214 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1215 ] 1216 1217 # add instruments count by type: 1218 for iType in self.iList.keys(): 1219 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1220 1221 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1222 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1223 1224 # generating info tables with all instruments by type: 1225 for iType in self.iList.keys(): 1226 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1227 1228 for instrument in self.iList[iType].keys(): 1229 iName = self.iList[iType][instrument]["name"] # instrument's name 1230 if len(iName) > 57: 1231 iName = "{}...".format(iName[:54]) # right trim for a long string 1232 1233 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1234 self.iList[iType][instrument]["ticker"], 1235 iName, 1236 self.iList[iType][instrument]["figi"], 1237 self.iList[iType][instrument]["currency"], 1238 self.iList[iType][instrument]["lot"], 1239 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1240 )) 1241 1242 infoText = "".join(info) 1243 1244 if show: 1245 uLogger.info(infoText) 1246 1247 if self.instrumentsFile: 1248 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1249 fH.write(infoText) 1250 1251 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1252 1253 if self.useHTMLReports: 1254 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1255 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1256 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1257 1258 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1259 1260 return infoText 1261 1262 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1263 """ 1264 This method search and show information about instruments by part of its ticker, FIGI or name. 1265 If `searchResultsFile` string is not empty then also save information to this file. 1266 1267 :param pattern: string with part of ticker, FIGI or instrument's name. 1268 :param show: if `True` then print results to console, if `False` — return list of result only. 1269 :return: list of dictionaries with all found instruments. 1270 """ 1271 if not self.iList: 1272 self.iList = self.Listing() 1273 1274 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1275 compiledPattern = re.compile(pattern, re.IGNORECASE) 1276 1277 for iType in self.iList: 1278 for instrument in self.iList[iType].values(): 1279 searchResult = compiledPattern.search(" ".join( 1280 [instrument["ticker"], instrument["figi"], instrument["name"]] 1281 )) 1282 1283 if searchResult: 1284 searchResults[iType][instrument["ticker"]] = instrument 1285 1286 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1287 info = [ 1288 "# Search results\n\n", 1289 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1290 "* **Search pattern:** [{}]\n".format(pattern), 1291 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1292 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1293 ] 1294 infoShort = info[:] 1295 1296 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1297 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1298 skippedLine = "| ... | ... | ... | ... |\n" 1299 1300 if resultsLen == 0: 1301 info.append("\nNo results\n") 1302 infoShort.append("\nNo results\n") 1303 uLogger.warning("No results. Try changing your search pattern.") 1304 1305 else: 1306 for iType in searchResults: 1307 iTypeValuesCount = len(searchResults[iType].values()) 1308 if iTypeValuesCount > 0: 1309 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1310 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1311 1312 for instrument in searchResults[iType].values(): 1313 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1314 instrument["type"], 1315 instrument["ticker"], 1316 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1317 instrument["figi"], 1318 )) 1319 1320 if iTypeValuesCount <= 5: 1321 infoShort.extend(info[-iTypeValuesCount:]) 1322 1323 else: 1324 infoShort.extend(info[-5:]) 1325 infoShort.append(skippedLine) 1326 1327 infoText = "".join(info) 1328 infoTextShort = "".join(infoShort) 1329 1330 if show: 1331 uLogger.info(infoTextShort) 1332 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1333 1334 if self.searchResultsFile: 1335 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1336 fH.write(infoText) 1337 1338 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1339 1340 if self.useHTMLReports: 1341 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1342 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1343 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1344 1345 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1346 1347 return searchResults 1348 1349 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1350 """ 1351 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1352 1353 :param instruments: list of strings with tickers or FIGIs. 1354 :return: list with unique instrument FIGIs only. 1355 """ 1356 requestedInstruments = [] 1357 for iName in instruments: 1358 if iName not in self.aliases.keys(): 1359 if iName not in requestedInstruments: 1360 requestedInstruments.append(iName) 1361 1362 else: 1363 if iName not in requestedInstruments: 1364 if self.aliases[iName] not in requestedInstruments: 1365 requestedInstruments.append(self.aliases[iName]) 1366 1367 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1368 1369 onlyUniqueFIGIs = [] 1370 for iName in requestedInstruments: 1371 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1372 continue 1373 1374 self._ticker = iName 1375 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1376 1377 if not iData: 1378 self._ticker = "" 1379 self._figi = iName 1380 1381 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1382 1383 if not iData: 1384 self._figi = "" 1385 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1386 1387 if iData and iData["figi"] not in onlyUniqueFIGIs: 1388 onlyUniqueFIGIs.append(iData["figi"]) 1389 1390 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1391 1392 return onlyUniqueFIGIs 1393 1394 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1395 """ 1396 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1397 1398 See limits: https://tinkoff.github.io/investAPI/limits/ 1399 1400 If `pricesFile` string is not empty then also save information to this file. 1401 1402 :param instruments: list of strings with tickers or FIGIs. 1403 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1404 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1405 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1406 """ 1407 if instruments is None or not instruments: 1408 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1409 raise Exception("Ticker or FIGI required") 1410 1411 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1412 1413 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1414 1415 iList = [] # trying to get info and current prices about all unique instruments: 1416 for self._figi in onlyUniqueFIGIs: 1417 iData = self.SearchByFIGI(requestPrice=True) 1418 iList.append(iData) 1419 1420 self.ShowListOfPrices(iList, show) 1421 1422 return iList 1423 1424 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1425 """ 1426 Show table contains current prices of given instruments. 1427 1428 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1429 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1430 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1431 :return: multilines text in Markdown format as a table contains current prices. 1432 """ 1433 infoText = "" 1434 1435 if show or self.pricesFile: 1436 info = [ 1437 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1438 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1439 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1440 ] 1441 1442 for item in iList: 1443 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1444 item["ticker"], 1445 item["figi"], 1446 item["type"], 1447 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1448 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1449 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1450 "{} / {}".format( 1451 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1452 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1453 ), 1454 "{} / {}".format( 1455 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1456 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1457 ), 1458 item["currency"], 1459 )) 1460 1461 infoText = "".join(info) 1462 1463 if show: 1464 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1465 1466 if self.pricesFile: 1467 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1468 fH.write(infoText) 1469 1470 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1471 1472 if self.useHTMLReports: 1473 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1474 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1475 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1476 1477 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1478 1479 return infoText 1480 1481 def RequestTradingStatus(self) -> dict: 1482 """ 1483 Requesting trading status for the instrument defined by `figi` variable. 1484 1485 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1486 1487 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1488 1489 :return: dictionary with trading status attributes. Response example: 1490 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1491 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1492 """ 1493 if self._figi is None or not self._figi: 1494 uLogger.error("Variable `figi` must be defined for using this method!") 1495 raise Exception("FIGI required") 1496 1497 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1498 1499 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1500 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1501 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1502 1503 if self.moreDebug: 1504 uLogger.debug("Records about current trading status successfully received") 1505 1506 return tradingStatus 1507 1508 def RequestPortfolio(self) -> dict: 1509 """ 1510 Requesting actual user's portfolio for current `accountId`. 1511 1512 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1513 1514 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1515 1516 :return: dictionary with user's portfolio. 1517 """ 1518 if self.accountId is None or not self.accountId: 1519 uLogger.error("Variable `accountId` must be defined for using this method!") 1520 raise Exception("Account ID required") 1521 1522 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1523 1524 self.body = str({"accountId": self.accountId}) 1525 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1526 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1527 1528 if self.moreDebug: 1529 uLogger.debug("Records about user's portfolio successfully received") 1530 1531 return rawPortfolio 1532 1533 def RequestPositions(self) -> dict: 1534 """ 1535 Requesting open positions by currencies and instruments for current `accountId`. 1536 1537 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1538 1539 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1540 1541 :return: dictionary with open positions by instruments. 1542 """ 1543 if self.accountId is None or not self.accountId: 1544 uLogger.error("Variable `accountId` must be defined for using this method!") 1545 raise Exception("Account ID required") 1546 1547 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1548 1549 self.body = str({"accountId": self.accountId}) 1550 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1551 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1552 1553 if self.moreDebug: 1554 uLogger.debug("Records about current open positions successfully received") 1555 1556 return rawPositions 1557 1558 def RequestPendingOrders(self) -> list: 1559 """ 1560 Requesting current actual pending limit orders for current `accountId`. 1561 1562 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1563 1564 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1565 1566 :return: list of dictionaries with pending limit orders. 1567 """ 1568 if self.accountId is None or not self.accountId: 1569 uLogger.error("Variable `accountId` must be defined for using this method!") 1570 raise Exception("Account ID required") 1571 1572 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1573 1574 self.body = str({"accountId": self.accountId}) 1575 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1576 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1577 1578 if "orders" in rawResponse.keys(): 1579 rawOrders = rawResponse["orders"] 1580 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1581 1582 else: 1583 rawOrders = [] 1584 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1585 1586 return rawOrders 1587 1588 def RequestStopOrders(self) -> list: 1589 """ 1590 Requesting current actual stop orders for current `accountId`. 1591 1592 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1593 1594 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1595 1596 :return: list of dictionaries with stop orders. 1597 """ 1598 if self.accountId is None or not self.accountId: 1599 uLogger.error("Variable `accountId` must be defined for using this method!") 1600 raise Exception("Account ID required") 1601 1602 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1603 1604 self.body = str({"accountId": self.accountId}) 1605 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1606 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1607 1608 if "stopOrders" in rawResponse.keys(): 1609 rawStopOrders = rawResponse["stopOrders"] 1610 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1611 1612 else: 1613 rawStopOrders = [] 1614 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1615 1616 return rawStopOrders 1617 1618 def Overview(self, show: bool = False, details: str = "full") -> dict: 1619 """ 1620 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1621 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1622 and `overviewBondsCalendarFile` are defined then also save information to file. 1623 1624 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1625 many requests about the state of the portfolio, and then, based on the received data, a large number 1626 of calculation and statistics are collected. 1627 1628 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1629 :param details: how detailed should the information be? 1630 - `full` — shows full available information about portfolio status (by default), 1631 - `positions` — shows only open positions, 1632 - `orders` — shows only sections of open limits and stop orders. 1633 - `digest` — show a short digest of the portfolio status, 1634 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1635 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1636 :return: dictionary with client's raw portfolio and some statistics. 1637 """ 1638 if self.accountId is None or not self.accountId: 1639 uLogger.error("Variable `accountId` must be defined for using this method!") 1640 raise Exception("Account ID required") 1641 1642 view = { 1643 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1644 "headers": {}, # list of dictionaries, response headers without "positions" section 1645 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1646 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1647 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1648 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1649 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1650 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1651 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1652 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1653 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1654 }, 1655 "stat": { # --- some statistics calculated using "raw" sections: 1656 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1657 "availableRUB": 0., # available rubles (without other currencies) 1658 "blockedRUB": 0., # blocked sum in Russian Rouble 1659 "totalChangesRUB": 0., # changes for all open trades in RUB 1660 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1661 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1662 "sharesCostRUB": 0., # costs of all shares in RUB 1663 "bondsCostRUB": 0., # costs of all bonds in RUB 1664 "etfsCostRUB": 0., # costs of all etfs in RUB 1665 "futuresCostRUB": 0., # costs of all futures in RUB 1666 "Currencies": [], # list of dictionaries of all currencies statistics 1667 "Shares": [], # list of dictionaries of all shares statistics 1668 "Bonds": [], # list of dictionaries of all bonds statistics 1669 "Etfs": [], # list of dictionaries of all etfs statistics 1670 "Futures": [], # list of dictionaries of all futures statistics 1671 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1672 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1673 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1674 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1675 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1676 }, 1677 "analytics": { # --- some analytics of portfolio: 1678 "distrByAssets": {}, # portfolio distribution by assets 1679 "distrByCompanies": {}, # portfolio distribution by companies 1680 "distrBySectors": {}, # portfolio distribution by sectors 1681 "distrByCurrencies": {}, # portfolio distribution by currencies 1682 "distrByCountries": {}, # portfolio distribution by countries 1683 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1684 } 1685 } 1686 1687 details = details.lower() 1688 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1689 if details not in availableDetails: 1690 details = "full" 1691 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1692 1693 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1694 1695 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1696 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1697 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1698 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1699 1700 # save response headers without "positions" section: 1701 for key in portfolioResponse.keys(): 1702 if key != "positions": 1703 view["raw"]["headers"][key] = portfolioResponse[key] 1704 1705 else: 1706 continue 1707 1708 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1709 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1710 for item in portfolioResponse["positions"]: 1711 if item["instrumentType"] == "currency": 1712 self._figi = item["figi"] 1713 if not self._figi and item["ticker"]: 1714 self._ticker = item["ticker"] 1715 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1716 1717 curr = self.SearchByFIGI(requestPrice=False) 1718 1719 # current price of currency in RUB: 1720 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1721 "name": curr["name"], 1722 "currentPrice": NanoToFloat( 1723 item["currentPrice"]["units"], 1724 item["currentPrice"]["nano"] 1725 ), 1726 } 1727 1728 view["raw"]["Currencies"].append(item) 1729 1730 elif item["instrumentType"] == "share": 1731 view["raw"]["Shares"].append(item) 1732 1733 elif item["instrumentType"] == "bond": 1734 view["raw"]["Bonds"].append(item) 1735 1736 elif item["instrumentType"] == "etf": 1737 view["raw"]["Etfs"].append(item) 1738 1739 elif item["instrumentType"] == "futures": 1740 view["raw"]["Futures"].append(item) 1741 1742 else: 1743 continue 1744 1745 # how many volume of currencies (by ISO currency name) are blocked: 1746 for item in view["raw"]["positions"]["blocked"]: 1747 blocked = NanoToFloat(item["units"], item["nano"]) 1748 if blocked > 0: 1749 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1750 1751 # how many volume of instruments (by FIGI) are blocked: 1752 for item in view["raw"]["positions"]["securities"]: 1753 blocked = int(item["blocked"]) 1754 if blocked > 0: 1755 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1756 1757 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1758 1759 if "rub" in allBlocked.keys(): 1760 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1761 1762 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1763 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1764 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1765 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1766 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1767 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1768 view["stat"]["portfolioCostRUB"] = sum([ 1769 view["stat"]["allCurrenciesCostRUB"], 1770 view["stat"]["sharesCostRUB"], 1771 view["stat"]["bondsCostRUB"], 1772 view["stat"]["etfsCostRUB"], 1773 view["stat"]["futuresCostRUB"], 1774 ]) 1775 1776 # --- calculating some portfolio statistics: 1777 byComp = {} # distribution by companies 1778 bySect = {} # distribution by sectors 1779 byCurr = {} # distribution by currencies (include RUB) 1780 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1781 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1782 1783 for item in portfolioResponse["positions"]: 1784 self._figi = item["figi"] 1785 if not self._figi and item["ticker"]: 1786 self._ticker = item["ticker"] 1787 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1788 1789 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1790 1791 if instrument: 1792 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1793 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1794 1795 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1796 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1797 1798 else: 1799 blocked = 0 1800 1801 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1802 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1803 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1804 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1805 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1806 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1807 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1808 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1809 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1810 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1811 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1812 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1813 1814 statData = { 1815 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1816 "ticker": instrument["ticker"], # ticker by FIGI 1817 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1818 "volume": volume, # available volume of instrument 1819 "lots": lots, # volume in lots of instrument 1820 "direction": direction, # direction of an instrument's position: short or long 1821 "blocked": blocked, # blocked volume of currency or instrument 1822 "currentPrice": curPrice, # current instrument's price in basic asset 1823 "average": average, # current average position price 1824 "cost": cost, # current cost of all volume of instrument in basic asset 1825 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1826 "costRUB": costRUB, # cost of instrument in ruble 1827 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1828 "profit": profit, # expected profit at current moment 1829 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1830 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1831 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1832 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1833 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1834 "step": instrument["step"], # minimum price increment 1835 } 1836 1837 # adding distribution by unique countries: 1838 if statData["country"] not in byCountry.keys(): 1839 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1840 1841 else: 1842 byCountry[statData["country"]]["cost"] += costRUB 1843 byCountry[statData["country"]]["percent"] += percentCostRUB 1844 1845 if item["instrumentType"] != "currency": 1846 # adding distribution by unique companies: 1847 if statData["name"]: 1848 if statData["name"] not in byComp.keys(): 1849 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1850 1851 else: 1852 byComp[statData["name"]]["cost"] += costRUB 1853 byComp[statData["name"]]["percent"] += percentCostRUB 1854 1855 # adding distribution by unique sectors: 1856 if statData["sector"] not in bySect.keys(): 1857 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1858 1859 else: 1860 bySect[statData["sector"]]["cost"] += costRUB 1861 bySect[statData["sector"]]["percent"] += percentCostRUB 1862 1863 # adding distribution by unique currencies: 1864 if currency not in byCurr.keys(): 1865 byCurr[currency] = { 1866 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1867 "cost": costRUB, 1868 "percent": percentCostRUB 1869 } 1870 1871 else: 1872 byCurr[currency]["cost"] += costRUB 1873 byCurr[currency]["percent"] += percentCostRUB 1874 1875 # saving statistics for every instrument: 1876 if item["instrumentType"] == "currency": 1877 view["stat"]["Currencies"].append(statData) 1878 1879 # update dict with free funds for trading (total - blocked) by currencies 1880 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1881 view["stat"]["funds"][currency] = { 1882 "total": volume, 1883 "totalCostRUB": costRUB, # total volume cost in rubles 1884 "free": volume - blocked, 1885 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1886 } 1887 1888 elif item["instrumentType"] == "share": 1889 view["stat"]["Shares"].append(statData) 1890 1891 elif item["instrumentType"] == "bond": 1892 view["stat"]["Bonds"].append(statData) 1893 1894 elif item["instrumentType"] == "etf": 1895 view["stat"]["Etfs"].append(statData) 1896 1897 elif item["instrumentType"] == "Futures": 1898 view["stat"]["Futures"].append(statData) 1899 1900 else: 1901 continue 1902 1903 # total changes in Russian Ruble: 1904 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1905 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1906 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1907 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1908 view["stat"]["funds"]["rub"] = { 1909 "total": view["stat"]["availableRUB"], 1910 "totalCostRUB": view["stat"]["availableRUB"], 1911 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1912 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1913 } 1914 1915 # --- pending limit orders sector data: 1916 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1917 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1918 1919 for item in view["raw"]["orders"]: 1920 self._figi = item["figi"] 1921 1922 if item["figi"] not in uniquePendingOrdersFIGIs: 1923 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1924 1925 uniquePendingOrdersFIGIs.append(item["figi"]) 1926 uniquePendingOrders[item["figi"]] = instrument 1927 1928 else: 1929 instrument = uniquePendingOrders[item["figi"]] 1930 1931 if instrument: 1932 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1933 orderType = TKS_ORDER_TYPES[item["orderType"]] 1934 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1935 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1936 1937 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1938 if item["direction"] == "ORDER_DIRECTION_BUY": 1939 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1940 1941 else: 1942 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1943 1944 # requested price for order execution: 1945 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1946 1947 # necessary changes in percent to reach target from current price: 1948 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1949 1950 view["stat"]["orders"].append({ 1951 "orderID": item["orderId"], # orderId number parameter of current order 1952 "figi": item["figi"], # FIGI identification 1953 "ticker": instrument["ticker"], # ticker name by FIGI 1954 "lotsRequested": item["lotsRequested"], # requested lots value 1955 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1956 "currentPrice": lastPrice, # current instrument's price for defined action 1957 "targetPrice": target, # requested price for order execution in base currency 1958 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1959 "percentChanges": changes, # changes in percent to target from current price 1960 "currency": item["currency"], # instrument's currency name 1961 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1962 "type": orderType, # type of order from TKS_ORDER_TYPES 1963 "status": orderState, # order status from TKS_ORDER_STATES 1964 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1965 }) 1966 1967 # --- stop orders sector data: 1968 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1969 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1970 1971 for item in view["raw"]["stopOrders"]: 1972 self._figi = item["figi"] 1973 1974 if item["figi"] not in uniqueStopOrdersFIGIs: 1975 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1976 1977 uniqueStopOrdersFIGIs.append(item["figi"]) 1978 uniqueStopOrders[item["figi"]] = instrument 1979 1980 else: 1981 instrument = uniqueStopOrders[item["figi"]] 1982 1983 if instrument: 1984 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1985 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1986 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1987 1988 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1989 if "expirationTime" in item.keys(): 1990 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1991 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1992 1993 else: 1994 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1995 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1996 1997 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1998 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1999 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2000 2001 else: 2002 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2003 2004 # requested price when stop-order executed: 2005 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2006 2007 # price for limit-order, set up when stop-order executed: 2008 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2009 2010 # necessary changes in percent to reach target from current price: 2011 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2012 2013 view["stat"]["stopOrders"].append({ 2014 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2015 "figi": item["figi"], # FIGI identification 2016 "ticker": instrument["ticker"], # ticker name by FIGI 2017 "lotsRequested": item["lotsRequested"], # requested lots value 2018 "currentPrice": lastPrice, # current instrument's price for defined action 2019 "targetPrice": target, # requested price for stop-order execution in base currency 2020 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2021 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2022 "percentChanges": changes, # changes in percent to target from current price 2023 "currency": item["currency"], # instrument's currency name 2024 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2025 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2026 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2027 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2028 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2029 }) 2030 2031 # --- calculating data for analytics section: 2032 # portfolio distribution by assets: 2033 view["analytics"]["distrByAssets"] = { 2034 "Ruble": { 2035 "uniques": 1, 2036 "cost": view["stat"]["availableRUB"], 2037 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2038 }, 2039 "Currencies": { 2040 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2041 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2042 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2043 }, 2044 "Shares": { 2045 "uniques": len(view["stat"]["Shares"]), 2046 "cost": view["stat"]["sharesCostRUB"], 2047 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2048 }, 2049 "Bonds": { 2050 "uniques": len(view["stat"]["Bonds"]), 2051 "cost": view["stat"]["bondsCostRUB"], 2052 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2053 }, 2054 "Etfs": { 2055 "uniques": len(view["stat"]["Etfs"]), 2056 "cost": view["stat"]["etfsCostRUB"], 2057 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2058 }, 2059 "Futures": { 2060 "uniques": len(view["stat"]["Futures"]), 2061 "cost": view["stat"]["futuresCostRUB"], 2062 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2063 }, 2064 } 2065 2066 # portfolio distribution by companies: 2067 view["analytics"]["distrByCompanies"]["All money cash"] = { 2068 "ticker": "", 2069 "cost": view["stat"]["allCurrenciesCostRUB"], 2070 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2071 } 2072 view["analytics"]["distrByCompanies"].update(byComp) 2073 2074 # portfolio distribution by sectors: 2075 view["analytics"]["distrBySectors"]["All money cash"] = { 2076 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2077 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2078 } 2079 view["analytics"]["distrBySectors"].update(bySect) 2080 2081 # portfolio distribution by currencies: 2082 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2083 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2084 2085 if self.moreDebug: 2086 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2087 2088 view["analytics"]["distrByCurrencies"].update(byCurr) 2089 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2090 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2091 2092 # portfolio distribution by countries: 2093 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2094 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2095 2096 if self.moreDebug: 2097 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2098 2099 view["analytics"]["distrByCountries"].update(byCountry) 2100 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2101 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2102 2103 # --- Prepare text statistics overview in human-readable: 2104 if show: 2105 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2106 2107 # Whatever the value `details`, header not changes: 2108 info = [ 2109 "# Client's portfolio\n\n", 2110 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2111 "* **Account ID:** [{}]\n".format(self.accountId), 2112 ] 2113 2114 if details in ["full", "positions", "digest"]: 2115 info.extend([ 2116 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2117 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2118 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2119 view["stat"]["totalChangesRUB"], 2120 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2121 view["stat"]["totalChangesPercentRUB"], 2122 ), 2123 ]) 2124 2125 if details in ["full", "positions"]: 2126 info.extend([ 2127 "## Open positions\n\n", 2128 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2129 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2130 "| **Ruble:** | {:>31} | | | | | |\n".format( 2131 "{:.2f} ({:.2f}) rub".format( 2132 view["stat"]["availableRUB"], 2133 view["stat"]["blockedRUB"], 2134 ) 2135 ) 2136 ]) 2137 2138 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2139 return [ 2140 "| | | | | | | |\n", 2141 "| {:<27} | | | | | {:>19} | |\n".format( 2142 noTradeStr if noTradeStr else typeStr, 2143 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2144 ), 2145 ] 2146 2147 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2148 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2149 "{} [{}]".format(data["ticker"], data["figi"]), 2150 "{:.2f} ({:.2f}) {}".format( 2151 data["volume"], 2152 data["blocked"], 2153 data["currency"], 2154 ) if isCurr else "{:.0f} ({:.0f})".format( 2155 data["volume"], 2156 data["blocked"], 2157 ), 2158 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2159 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2160 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2161 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2162 "{}{:.2f} {} ({}{:.2f}%)".format( 2163 "+" if data["profit"] > 0 else "", 2164 data["profit"], data["baseCurrencyName"], 2165 "+" if data["percentProfit"] > 0 else "", 2166 data["percentProfit"], 2167 ), 2168 ) 2169 2170 # --- Show currencies section: 2171 if view["stat"]["Currencies"]: 2172 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2173 for item in view["stat"]["Currencies"]: 2174 info.append(_InfoStr(item, isCurr=True)) 2175 2176 else: 2177 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2178 2179 # --- Show shares section: 2180 if view["stat"]["Shares"]: 2181 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2182 2183 for item in view["stat"]["Shares"]: 2184 info.append(_InfoStr(item)) 2185 2186 else: 2187 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2188 2189 # --- Show bonds section: 2190 if view["stat"]["Bonds"]: 2191 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2192 2193 for item in view["stat"]["Bonds"]: 2194 info.append(_InfoStr(item)) 2195 2196 else: 2197 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2198 2199 # --- Show etfs section: 2200 if view["stat"]["Etfs"]: 2201 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2202 2203 for item in view["stat"]["Etfs"]: 2204 info.append(_InfoStr(item)) 2205 2206 else: 2207 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2208 2209 # --- Show futures section: 2210 if view["stat"]["Futures"]: 2211 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2212 2213 for item in view["stat"]["Futures"]: 2214 info.append(_InfoStr(item)) 2215 2216 else: 2217 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2218 2219 if details in ["full", "orders"]: 2220 # --- Show pending limit orders section: 2221 if view["stat"]["orders"]: 2222 info.extend([ 2223 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2224 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2225 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2226 ]) 2227 2228 for item in view["stat"]["orders"]: 2229 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2230 "{} [{}]".format(item["ticker"], item["figi"]), 2231 item["orderID"], 2232 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2233 "{} {} ({}{:.2f}%)".format( 2234 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2235 item["baseCurrencyName"], 2236 "+" if item["percentChanges"] > 0 else "", 2237 float(item["percentChanges"]), 2238 ), 2239 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2240 item["action"], 2241 item["type"], 2242 item["date"], 2243 )) 2244 2245 else: 2246 info.append("\n## Total pending limit-orders: [0]\n") 2247 2248 # --- Show stop orders section: 2249 if view["stat"]["stopOrders"]: 2250 info.extend([ 2251 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2252 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2253 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2254 ]) 2255 2256 for item in view["stat"]["stopOrders"]: 2257 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2258 "{} [{}]".format(item["ticker"], item["figi"]), 2259 item["orderID"], 2260 item["lotsRequested"], 2261 "{} {} ({}{:.2f}%)".format( 2262 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2263 item["baseCurrencyName"], 2264 "+" if item["percentChanges"] > 0 else "", 2265 float(item["percentChanges"]), 2266 ), 2267 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2268 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2269 item["action"], 2270 item["type"], 2271 item["expType"], 2272 item["createDate"], 2273 item["expDate"], 2274 )) 2275 2276 else: 2277 info.append("\n## Total stop-orders: [0]\n") 2278 2279 if details in ["full", "analytics"]: 2280 # -- Show analytics section: 2281 if view["stat"]["portfolioCostRUB"] > 0: 2282 info.extend([ 2283 "\n# Analytics\n\n" 2284 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2285 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2286 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2287 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2288 view["stat"]["totalChangesRUB"], 2289 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2290 view["stat"]["totalChangesPercentRUB"], 2291 ), 2292 "\n## Portfolio distribution by assets\n" 2293 "\n| Type | Uniques | Percent | Current cost |\n", 2294 "|------------------------------------|---------|---------|--------------------|\n", 2295 ]) 2296 2297 for key in view["analytics"]["distrByAssets"].keys(): 2298 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2299 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2300 key, 2301 view["analytics"]["distrByAssets"][key]["uniques"], 2302 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2303 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2304 )) 2305 2306 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2307 2308 info.extend([ 2309 "\n## Portfolio distribution by companies\n" 2310 "\n| Company | Percent | Current cost |\n", 2311 aSepLine, 2312 ]) 2313 2314 for company in view["analytics"]["distrByCompanies"].keys(): 2315 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2316 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2317 "{}{}".format( 2318 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2319 company, 2320 ), 2321 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2322 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2323 )) 2324 2325 info.extend([ 2326 "\n## Portfolio distribution by sectors\n" 2327 "\n| Sector | Percent | Current cost |\n", 2328 aSepLine, 2329 ]) 2330 2331 for sector in view["analytics"]["distrBySectors"].keys(): 2332 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2333 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2334 sector, 2335 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2336 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2337 )) 2338 2339 info.extend([ 2340 "\n## Portfolio distribution by currencies\n" 2341 "\n| Instruments currencies | Percent | Current cost |\n", 2342 aSepLine, 2343 ]) 2344 2345 for curr in view["analytics"]["distrByCurrencies"].keys(): 2346 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2347 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2348 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2349 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2350 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2351 )) 2352 2353 info.extend([ 2354 "\n## Portfolio distribution by countries\n" 2355 "\n| Assets by country | Percent | Current cost |\n", 2356 aSepLine, 2357 ]) 2358 2359 for country in view["analytics"]["distrByCountries"].keys(): 2360 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2361 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2362 country, 2363 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2364 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2365 )) 2366 2367 if details in ["full", "calendar"]: 2368 # -- Show bonds payment calendar section: 2369 if view["stat"]["Bonds"]: 2370 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2371 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2372 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2373 2374 else: 2375 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2376 2377 infoText = "".join(info) 2378 2379 uLogger.info(infoText) 2380 2381 if details == "full" and self.overviewFile: 2382 filename = self.overviewFile 2383 2384 elif details == "digest" and self.overviewDigestFile: 2385 filename = self.overviewDigestFile 2386 2387 elif details == "positions" and self.overviewPositionsFile: 2388 filename = self.overviewPositionsFile 2389 2390 elif details == "orders" and self.overviewOrdersFile: 2391 filename = self.overviewOrdersFile 2392 2393 elif details == "analytics" and self.overviewAnalyticsFile: 2394 filename = self.overviewAnalyticsFile 2395 2396 elif details == "calendar" and self.overviewBondsCalendarFile: 2397 filename = self.overviewBondsCalendarFile 2398 2399 else: 2400 filename = "" 2401 2402 if filename: 2403 with open(filename, "w", encoding="UTF-8") as fH: 2404 fH.write(infoText) 2405 2406 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2407 2408 if self.useHTMLReports: 2409 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2410 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2411 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2412 2413 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2414 2415 return view 2416 2417 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2418 """ 2419 Returns history operations between two given dates for current `accountId`. 2420 If `reportFile` string is not empty then also save human-readable report. 2421 Shows some statistical data of closed positions. 2422 2423 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2424 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2425 :param show: if `True` then also prints all records to the console. 2426 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2427 :return: original list of dictionaries with history of deals records from API ("operations" key): 2428 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2429 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2430 """ 2431 if self.accountId is None or not self.accountId: 2432 uLogger.error("Variable `accountId` must be defined for using this method!") 2433 raise Exception("Account ID required") 2434 2435 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2436 2437 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2438 2439 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2440 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2441 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2442 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2443 customStat = {} # custom statistics in additional to responseJSON 2444 2445 # --- output report in human-readable format: 2446 if show or self.reportFile: 2447 splitLine1 = "| | | | | |\n" # Summary section 2448 splitLine2 = "| | | | | | | | |\n" # Operations section 2449 nextDay = "" 2450 2451 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2452 2453 if len(ops) > 0: 2454 customStat = { 2455 "opsCount": 0, # total operations count 2456 "buyCount": 0, # buy operations 2457 "sellCount": 0, # sell operations 2458 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2459 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2460 "payIn": {"rub": 0.}, # Deposit brokerage account 2461 "payOut": {"rub": 0.}, # Withdrawals 2462 "divs": {"rub": 0.}, # Dividends income 2463 "coupons": {"rub": 0.}, # Coupon's income 2464 "brokerCom": {"rub": 0.}, # Service commissions 2465 "serviceCom": {"rub": 0.}, # Service commissions 2466 "marginCom": {"rub": 0.}, # Margin commissions 2467 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2468 } 2469 2470 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2471 for item in ops: 2472 if item["state"] == "OPERATION_STATE_EXECUTED": 2473 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2474 2475 # count buy operations: 2476 if "_BUY" in item["operationType"]: 2477 customStat["buyCount"] += 1 2478 2479 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2480 customStat["buyTotal"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["buyTotal"][item["payment"]["currency"]] = payment 2484 2485 # count sell operations: 2486 elif "_SELL" in item["operationType"]: 2487 customStat["sellCount"] += 1 2488 2489 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2490 customStat["sellTotal"][item["payment"]["currency"]] += payment 2491 2492 else: 2493 customStat["sellTotal"][item["payment"]["currency"]] = payment 2494 2495 # count incoming operations: 2496 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2497 if item["payment"]["currency"] in customStat["payIn"].keys(): 2498 customStat["payIn"][item["payment"]["currency"]] += payment 2499 2500 else: 2501 customStat["payIn"][item["payment"]["currency"]] = payment 2502 2503 # count withdrawals operations: 2504 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2505 if item["payment"]["currency"] in customStat["payOut"].keys(): 2506 customStat["payOut"][item["payment"]["currency"]] += payment 2507 2508 else: 2509 customStat["payOut"][item["payment"]["currency"]] = payment 2510 2511 # count dividends income: 2512 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2513 if item["payment"]["currency"] in customStat["divs"].keys(): 2514 customStat["divs"][item["payment"]["currency"]] += payment 2515 2516 else: 2517 customStat["divs"][item["payment"]["currency"]] = payment 2518 2519 # count coupon's income: 2520 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2521 if item["payment"]["currency"] in customStat["coupons"].keys(): 2522 customStat["coupons"][item["payment"]["currency"]] += payment 2523 2524 else: 2525 customStat["coupons"][item["payment"]["currency"]] = payment 2526 2527 # count broker commissions: 2528 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2529 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2530 customStat["brokerCom"][item["payment"]["currency"]] += payment 2531 2532 else: 2533 customStat["brokerCom"][item["payment"]["currency"]] = payment 2534 2535 # count service commissions: 2536 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2537 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2538 customStat["serviceCom"][item["payment"]["currency"]] += payment 2539 2540 else: 2541 customStat["serviceCom"][item["payment"]["currency"]] = payment 2542 2543 # count margin commissions: 2544 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2545 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2546 customStat["marginCom"][item["payment"]["currency"]] += payment 2547 2548 else: 2549 customStat["marginCom"][item["payment"]["currency"]] = payment 2550 2551 # count withholding taxes: 2552 elif "_TAX" in item["operationType"]: 2553 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2554 customStat["allTaxes"][item["payment"]["currency"]] += payment 2555 2556 else: 2557 customStat["allTaxes"][item["payment"]["currency"]] = payment 2558 2559 else: 2560 continue 2561 2562 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2563 2564 # --- view "Actions" lines: 2565 info.extend([ 2566 "| Report sections | | | | |\n", 2567 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2568 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2569 "| | Buy: {:<22} | {:<28} | | |\n".format( 2570 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2571 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2572 ), 2573 "| | Sell: {:<21} | {:<28} | | |\n".format( 2574 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2575 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2576 ), 2577 ]) 2578 2579 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2580 for key in opsKeys: 2581 if key == "rub": 2582 continue 2583 2584 info.extend([ 2585 "| | | {:<28} | | |\n".format( 2586 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2587 ), 2588 "| | | {:<28} | | |\n".format( 2589 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2590 ), 2591 ]) 2592 2593 info.append(splitLine1) 2594 2595 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2596 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2597 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2598 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2599 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2600 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2601 ) 2602 2603 # --- view "Payments" lines: 2604 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2605 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2606 2607 for key in paymentsKeys: 2608 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2609 2610 info.append(splitLine1) 2611 2612 # --- view "Commissions and taxes" lines: 2613 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2614 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2615 2616 for key in comKeys: 2617 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2618 2619 info.extend([ 2620 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2621 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2622 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2623 ]) 2624 2625 else: 2626 info.append("Broker returned no operations during this period\n") 2627 2628 # --- view "Operations" section: 2629 for item in ops: 2630 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2631 continue 2632 2633 else: 2634 self._figi = item["figi"] 2635 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2636 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2637 2638 # group of deals during one day: 2639 if nextDay and item["date"].split("T")[0] != nextDay: 2640 info.append(splitLine2) 2641 nextDay = "" 2642 2643 else: 2644 nextDay = item["date"].split("T")[0] # saving current day for splitting 2645 2646 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2647 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2648 self._figi if self._figi else "—", 2649 instrument["ticker"] if instrument else "—", 2650 instrument["type"] if instrument else "—", 2651 item["quantity"] if int(item["quantity"]) > 0 else "—", 2652 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2653 TKS_OPERATION_STATES[item["state"]], 2654 TKS_OPERATION_TYPES[item["operationType"]], 2655 )) 2656 2657 infoText = "".join(info) 2658 2659 if show: 2660 if self.moreDebug: 2661 uLogger.debug("Records about history of a client's operations successfully received") 2662 2663 uLogger.info(infoText) 2664 2665 if self.reportFile: 2666 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2667 fH.write(infoText) 2668 2669 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2670 2671 if self.useHTMLReports: 2672 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2673 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2674 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2675 2676 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2677 2678 return ops, customStat 2679 2680 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2681 """ 2682 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2683 2684 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2685 Warning! Broker server used ISO UTC time by default. 2686 2687 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2688 Also, `historyFile` used to update history with `onlyMissing` parameter. 2689 2690 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2691 2692 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2693 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2694 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2695 `"hour"`, `"day"`. Default: `"hour"`. 2696 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2697 False by default. Warning! History appends only from last candle to current time 2698 with always update last candle! 2699 :param csvSep: separator if csv-file is used, `,` by default. 2700 :param show: if `True` then also prints Pandas DataFrame to the console. 2701 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2702 `["date", "time", "open", "high", "low", "close", "volume"]`. 2703 """ 2704 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2705 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2706 history = None # empty pandas object for history 2707 2708 if interval not in TKS_CANDLE_INTERVALS.keys(): 2709 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2710 raise Exception("Incorrect value") 2711 2712 if not (self._ticker or self._figi): 2713 uLogger.error("Ticker or FIGI must be defined!") 2714 raise Exception("Ticker or FIGI required") 2715 2716 if self._ticker and not self._figi: 2717 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2718 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2719 2720 if self._figi and not self._ticker: 2721 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2722 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2723 2724 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2725 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2726 if interval.lower() != "day": 2727 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2728 2729 delta = dtEnd - dtStart # current UTC time minus last time in file 2730 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2731 2732 # calculate history length in candles: 2733 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2734 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2735 length += 1 # to avoid fraction time 2736 2737 # calculate data blocks count: 2738 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2739 2740 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2741 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2742 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2743 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2744 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2745 2746 tempOld = None # pandas object for old history, if --only-missing key present 2747 lastTime = None # datetime object of last old candle in file 2748 2749 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2750 uLogger.debug("--only-missing key present, add only last missing candles...") 2751 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2752 2753 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2754 2755 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2756 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2757 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2758 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2759 2760 # get last datetime object from last string in file or minus 1 delta if file is empty: 2761 if len(tempOld) > 0: 2762 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2763 2764 else: 2765 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2766 2767 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2768 2769 responseJSONs = [] # raw history blocks of data 2770 2771 blockEnd = dtEnd 2772 for item in range(blocks): 2773 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2774 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2775 2776 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2777 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2778 )) 2779 2780 if blockStart == blockEnd: 2781 uLogger.debug("Skipped this zero-length block...") 2782 2783 else: 2784 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2785 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2786 self.body = str({ 2787 "figi": self._figi, 2788 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2789 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2790 "interval": TKS_CANDLE_INTERVALS[interval][0] 2791 }) 2792 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2793 2794 if "code" in responseJSON.keys(): 2795 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2796 2797 else: 2798 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2799 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2800 2801 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2802 2803 blockEnd = blockStart 2804 2805 printCount = len(responseJSONs) # candles to show in console 2806 if responseJSONs: 2807 tempHistory = pd.DataFrame( 2808 data={ 2809 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2810 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2811 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2812 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2813 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2814 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2815 "volume": [int(item["volume"]) for item in responseJSONs], 2816 }, 2817 index=range(len(responseJSONs)), 2818 columns=["date", "time", "open", "high", "low", "close", "volume"], 2819 ) 2820 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2821 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2822 2823 # append only newest candles to old history if --only-missing key present: 2824 if onlyMissing and tempOld is not None and lastTime is not None: 2825 index = 0 # find start index in tempHistory data: 2826 2827 for i, item in tempHistory.iterrows(): 2828 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2829 2830 if curTime == lastTime: 2831 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2832 index = i 2833 printCount = index + 1 2834 break 2835 2836 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2837 2838 else: 2839 history = tempHistory # if no `--only-missing` key then load full data from server 2840 2841 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2842 2843 if history is not None and not history.empty: 2844 if show: 2845 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2846 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2847 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2848 )) 2849 2850 else: 2851 uLogger.warning("Received an empty candles history!") 2852 2853 if self.historyFile is not None: 2854 if history is not None and not history.empty: 2855 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2856 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2857 2858 else: 2859 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2860 2861 else: 2862 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2863 2864 return history 2865 2866 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2867 """ 2868 Load candles history from csv-file and return Pandas DataFrame object. 2869 2870 See also: `History()` and `ShowHistoryChart()` methods. 2871 2872 :param filePath: path to csv-file to open. 2873 """ 2874 loadedHistory = None # init candles data object 2875 2876 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2877 2878 if os.path.exists(filePath): 2879 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2880 2881 tfStr = self.priceModel.FormattedDelta( 2882 self.priceModel.timeframe, 2883 "{days} days {hours}h {minutes}m {seconds}s", 2884 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2885 self.priceModel.timeframe, 2886 "{hours}h {minutes}m {seconds}s", 2887 ) 2888 2889 if loadedHistory is not None and not loadedHistory.empty: 2890 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2891 len(loadedHistory), 2892 tfStr, 2893 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2894 ) 2895 2896 else: 2897 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2898 2899 else: 2900 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2901 2902 return loadedHistory 2903 2904 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2905 """ 2906 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2907 2908 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2909 Default: `index.html` (both for interact and non-interact candlesticks chart). 2910 2911 See also: `History()` and `LoadHistory()` methods. 2912 2913 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2914 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2915 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2916 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2917 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2918 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2919 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2920 """ 2921 if isinstance(candles, str): 2922 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2923 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2924 2925 elif isinstance(candles, pd.DataFrame): 2926 self.priceModel.prices = candles # set candles chain from variable 2927 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2928 2929 if "datetime" not in candles.columns: 2930 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2931 2932 else: 2933 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2934 raise Exception("Incorrect value") 2935 2936 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2937 2938 if interact: 2939 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2940 2941 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2942 2943 else: 2944 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2945 2946 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2947 2948 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2949 2950 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2951 """ 2952 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2953 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2954 2955 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2956 2957 :param operation: string "Buy" or "Sell". 2958 :param lots: volume, integer count of lots >= 1. 2959 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2960 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2961 :param expDate: string "Undefined" by default or local date in future, 2962 it is a string with format `%Y-%m-%d %H:%M:%S`. 2963 :return: JSON with response from broker server. 2964 """ 2965 if self.accountId is None or not self.accountId: 2966 uLogger.error("Variable `accountId` must be defined for using this method!") 2967 raise Exception("Account ID required") 2968 2969 if operation is None or not operation or operation not in ("Buy", "Sell"): 2970 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2971 raise Exception("Incorrect value") 2972 2973 if lots is None or lots < 1: 2974 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2975 lots = 1 2976 2977 if tp is None or tp < 0: 2978 tp = 0 2979 2980 if sl is None or sl < 0: 2981 sl = 0 2982 2983 if expDate is None or not expDate: 2984 expDate = "Undefined" 2985 2986 if not (self._ticker or self._figi): 2987 uLogger.error("Ticker or FIGI must be defined!") 2988 raise Exception("Ticker or FIGI required") 2989 2990 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2991 self._ticker = instrument["ticker"] 2992 self._figi = instrument["figi"] 2993 2994 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2995 2996 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2997 self.body = str({ 2998 "figi": self._figi, 2999 "quantity": str(lots), 3000 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3001 "accountId": str(self.accountId), 3002 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3003 }) 3004 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3005 3006 if "orderId" in response.keys(): 3007 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3008 operation, response["orderId"], 3009 self._ticker, self._figi, lots, 3010 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3011 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3012 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3013 )) 3014 3015 if tp > 0: 3016 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3017 3018 if sl > 0: 3019 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3020 3021 else: 3022 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3023 3024 return response 3025 3026 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3027 """ 3028 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3029 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3030 3031 See also: `Order()` and `Trade()` docstrings. 3032 3033 :param lots: volume, integer count of lots >= 1. 3034 :param tp: float > 0, take profit price of stop-order. 3035 :param sl: float > 0, stop loss price of stop-order. 3036 :param expDate: it's a local date in future. 3037 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3038 :return: JSON with response from broker server. 3039 """ 3040 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3041 3042 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3043 """ 3044 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3045 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3046 3047 See also: `Order()` and `Trade()` docstrings. 3048 3049 :param lots: volume, integer count of lots >= 1. 3050 :param tp: float > 0, take profit price of stop-order. 3051 :param sl: float > 0, stop loss price of stop-order. 3052 :param expDate: it's a local date in the future. 3053 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3054 :return: JSON with response from broker server. 3055 """ 3056 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3057 3058 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3059 """ 3060 Close position of given instruments. 3061 3062 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3063 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3064 This avoids unnecessary downloading data from the server. 3065 """ 3066 if instruments is None or not instruments: 3067 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3068 raise Exception("Ticker or FIGI required") 3069 3070 if isinstance(instruments, str): 3071 instruments = [instruments] 3072 3073 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3074 if uniqueInstruments: 3075 if portfolio is None or not portfolio: 3076 portfolio = self.Overview(show=False) 3077 3078 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3079 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3080 3081 for self._figi in uniqueInstruments: 3082 if self._figi not in allOpened: 3083 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3084 continue 3085 3086 # search open trade info about instrument by ticker: 3087 instrument = {} 3088 for iType in TKS_INSTRUMENTS: 3089 if instrument: 3090 break 3091 3092 for item in portfolio["stat"][iType]: 3093 if item["figi"] == self._figi: 3094 instrument = item 3095 break 3096 3097 if instrument: 3098 self._ticker = instrument["ticker"] 3099 self._figi = instrument["figi"] 3100 3101 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3102 self._ticker, 3103 self._figi, 3104 int(instrument["volume"]), 3105 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3106 )) 3107 3108 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3109 3110 if tradeLots > 0: 3111 if instrument["blocked"] > 0: 3112 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3113 instrument["blocked"], 3114 self._ticker, 3115 tradeLots, 3116 )) 3117 3118 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3119 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3120 3121 else: 3122 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3123 3124 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3125 """ 3126 Close all positions of given instruments with defined type. 3127 3128 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3129 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3130 This avoids unnecessary downloading data from the server. 3131 """ 3132 if iType not in TKS_INSTRUMENTS: 3133 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3134 3135 else: 3136 if portfolio is None or not portfolio: 3137 portfolio = self.Overview(show=False) 3138 3139 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3140 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3141 3142 if tickers and portfolio: 3143 self.CloseTrades(tickers, portfolio) 3144 3145 else: 3146 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3147 3148 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3149 """ 3150 Universal method to create market or limit orders with all available parameters for current `accountId`. 3151 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3152 3153 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3154 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3155 3156 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3157 then broker immediately open market order as you can do simple --buy or --sell operations! 3158 3159 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3160 When current price will go up or down to target price value then broker opens a limit order. 3161 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3162 3163 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3164 3165 :param operation: string "Buy" or "Sell". 3166 :param orderType: string "Limit" or "Stop". 3167 :param lots: volume, integer count of lots >= 1. 3168 :param targetPrice: target price > 0. This is open trade price for limit order. 3169 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3170 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3171 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3172 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3173 Stop loss order always executed by market price. 3174 :param expDate: string "Undefined" by default or local date in future. 3175 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3176 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3177 A limit order has no expiration date, it lasts until the end of the trading day. 3178 :return: JSON with response from broker server. 3179 """ 3180 if self.accountId is None or not self.accountId: 3181 uLogger.error("Variable `accountId` must be defined for using this method!") 3182 raise Exception("Account ID required") 3183 3184 if operation is None or not operation or operation not in ("Buy", "Sell"): 3185 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3186 raise Exception("Incorrect value") 3187 3188 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3189 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3190 raise Exception("Incorrect value") 3191 3192 if lots is None or lots < 1: 3193 uLogger.error("You must define trade volume > 0: integer count of lots!") 3194 raise Exception("Incorrect value") 3195 3196 if targetPrice is None or targetPrice <= 0: 3197 uLogger.error("Target price for limit-order must be greater than 0!") 3198 raise Exception("Incorrect value") 3199 3200 if limitPrice is None or limitPrice <= 0: 3201 limitPrice = targetPrice 3202 3203 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3204 stopType = "Limit" 3205 3206 if expDate is None or not expDate: 3207 expDate = "Undefined" 3208 3209 if not (self._ticker or self._figi): 3210 uLogger.error("Tocker or FIGI must be defined!") 3211 raise Exception("Ticker or FIGI required") 3212 3213 response = {} 3214 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3215 self._ticker = instrument["ticker"] 3216 self._figi = instrument["figi"] 3217 3218 if orderType == "Limit": 3219 uLogger.debug( 3220 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3221 self._ticker, self._figi, 3222 operation, lots, targetPrice, instrument["currency"], 3223 )) 3224 3225 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3226 self.body = str({ 3227 "figi": self._figi, 3228 "quantity": str(lots), 3229 "price": FloatToNano(targetPrice), 3230 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3231 "accountId": str(self.accountId), 3232 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3233 }) 3234 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3235 3236 if "orderId" in response.keys(): 3237 uLogger.info( 3238 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3239 response["orderId"], self._ticker, self._figi, operation, lots, 3240 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3241 )) 3242 3243 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3244 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3245 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3246 targetPrice, instrument["currency"], 3247 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3248 )) 3249 3250 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3251 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3252 targetPrice, instrument["currency"], 3253 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3254 )) 3255 3256 else: 3257 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3258 3259 if orderType == "Stop": 3260 uLogger.debug( 3261 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3262 self._ticker, self._figi, 3263 operation, lots, 3264 targetPrice, instrument["currency"], 3265 limitPrice, instrument["currency"], 3266 stopType, expDate, 3267 )) 3268 3269 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3270 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3271 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3272 3273 body = { 3274 "figi": self._figi, 3275 "quantity": str(lots), 3276 "price": FloatToNano(limitPrice), 3277 "stopPrice": FloatToNano(targetPrice), 3278 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3279 "accountId": str(self.accountId), 3280 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3281 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3282 } 3283 3284 if expDateUTC: 3285 body["expireDate"] = expDateUTC 3286 3287 self.body = str(body) 3288 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3289 3290 if "stopOrderId" in response.keys(): 3291 uLogger.info( 3292 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3293 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3294 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3295 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3296 TKS_STOP_ORDER_TYPES[stopOrderType], 3297 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3298 )) 3299 3300 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3301 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3302 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3303 targetPrice, instrument["currency"], 3304 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3305 )) 3306 3307 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3308 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3309 targetPrice, instrument["currency"], 3310 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3311 )) 3312 3313 else: 3314 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3315 3316 return response 3317 3318 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3319 """ 3320 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3321 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3322 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3323 See also: `Order()` docstring. 3324 3325 :param lots: volume, integer count of lots >= 1. 3326 :param targetPrice: target price > 0. This is open trade price for limit order. 3327 :return: JSON with response from broker server. 3328 """ 3329 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3330 3331 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3332 """ 3333 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3334 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3335 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3336 target price value then broker opens a limit order. See also: `Order()` docstring. 3337 3338 :param lots: volume, integer count of lots >= 1. 3339 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3340 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3341 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3342 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3343 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3344 :param expDate: string "Undefined" by default or local date in future. 3345 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3346 This date is converting to UTC format for server. 3347 :return: JSON with response from broker server. 3348 """ 3349 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3350 3351 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3352 """ 3353 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3354 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3355 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3356 See also: `Order()` docstring. 3357 3358 :param lots: volume, integer count of lots >= 1. 3359 :param targetPrice: target price > 0. This is open trade price for limit order. 3360 :return: JSON with response from broker server. 3361 """ 3362 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3363 3364 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3365 """ 3366 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3367 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3368 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3369 target price value then broker opens a limit order. See also: `Order()` docstring. 3370 3371 :param lots: volume, integer count of lots >= 1. 3372 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3373 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3374 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3375 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3376 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3377 :param expDate: string "Undefined" by default or local date in future. 3378 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3379 This date is converting to UTC format for server. 3380 :return: JSON with response from broker server. 3381 """ 3382 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3383 3384 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3385 """ 3386 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3387 3388 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3389 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3390 This avoids unnecessary downloading data from the server. 3391 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3392 """ 3393 if self.accountId is None or not self.accountId: 3394 uLogger.error("Variable `accountId` must be defined for using this method!") 3395 raise Exception("Account ID required") 3396 3397 if orderIDs: 3398 if allOrdersIDs is None: 3399 rawOrders = self.RequestPendingOrders() 3400 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3401 3402 if allStopOrdersIDs is None: 3403 rawStopOrders = self.RequestStopOrders() 3404 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3405 3406 for orderID in orderIDs: 3407 idInPendingOrders = orderID in allOrdersIDs 3408 idInStopOrders = orderID in allStopOrdersIDs 3409 3410 if not (idInPendingOrders or idInStopOrders): 3411 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3412 continue 3413 3414 else: 3415 if idInPendingOrders: 3416 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3417 3418 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3419 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3420 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3421 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3422 3423 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3424 if self.moreDebug: 3425 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3426 3427 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3428 3429 else: 3430 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3431 3432 elif idInStopOrders: 3433 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3434 3435 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3436 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3437 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3438 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3439 3440 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3441 if self.moreDebug: 3442 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3443 3444 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3445 3446 else: 3447 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3448 3449 else: 3450 continue 3451 3452 def CloseAllOrders(self) -> None: 3453 """ 3454 Gets a list of open pending and stop orders and cancel it all. 3455 """ 3456 rawOrders = self.RequestPendingOrders() 3457 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3458 lenOrders = len(allOrdersIDs) 3459 3460 rawStopOrders = self.RequestStopOrders() 3461 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3462 lenSOrders = len(allStopOrdersIDs) 3463 3464 if lenOrders > 0 or lenSOrders > 0: 3465 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3466 3467 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3468 3469 else: 3470 uLogger.info("Orders not found, nothing to cancel.") 3471 3472 def CloseAll(self, *args) -> None: 3473 """ 3474 Close all available (not blocked) opened trades and orders. 3475 3476 Also, you can select one or more keywords case-insensitive: 3477 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3478 3479 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3480 """ 3481 overview = self.Overview(show=False) # get all open trades info 3482 3483 if len(args) == 0: 3484 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3485 self.CloseAllOrders() # close all pending and stop orders 3486 3487 for iType in TKS_INSTRUMENTS: 3488 if iType != "Currencies": 3489 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3490 3491 else: 3492 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3493 lowerArgs = [x.lower() for x in args] 3494 3495 if "orders" in lowerArgs: 3496 self.CloseAllOrders() # close all pending and stop orders 3497 3498 for iType in TKS_INSTRUMENTS: 3499 if iType.lower() in lowerArgs and iType != "Currencies": 3500 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3501 3502 def CloseAllByTicker(self, instrument: str) -> None: 3503 """ 3504 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3505 3506 This method searches opened trade and orders of instrument throw all portfolio and then use 3507 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3508 3509 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3510 3511 :param instrument: string with ticker. 3512 """ 3513 if instrument is None or not instrument: 3514 uLogger.error("Ticker name must be defined for using this method!") 3515 raise Exception("Ticker required") 3516 3517 overview = self.Overview(show=False) # get user portfolio with all open trades info 3518 3519 self._ticker = instrument # try to set instrument as ticker 3520 self._figi = "" 3521 3522 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3523 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3524 3525 if limitAll and self.IsInLimitOrders(portfolio=overview): 3526 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3527 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3528 3529 if stopAll and self.IsInStopOrders(portfolio=overview): 3530 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3531 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3532 3533 if self.IsInPortfolio(portfolio=overview): 3534 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3535 self.CloseTrades(instruments=[instrument], portfolio=overview) 3536 3537 def CloseAllByFIGI(self, instrument: str) -> None: 3538 """ 3539 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3540 3541 This method searches opened trade and orders of instrument throw all portfolio and then use 3542 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3543 3544 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3545 3546 :param instrument: string with FIGI id. 3547 """ 3548 if instrument is None or not instrument: 3549 uLogger.error("FIGI id must be defined for using this method!") 3550 raise Exception("FIGI required") 3551 3552 overview = self.Overview(show=False) # get user portfolio with all open trades info 3553 3554 self._ticker = "" 3555 self._figi = instrument # try to set instrument as FIGI id 3556 3557 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3558 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3559 3560 if limitAll and self.IsInLimitOrders(portfolio=overview): 3561 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3562 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3563 3564 if stopAll and self.IsInStopOrders(portfolio=overview): 3565 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3566 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3567 3568 if self.IsInPortfolio(portfolio=overview): 3569 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3570 self.CloseTrades(instruments=[instrument], portfolio=overview) 3571 3572 @staticmethod 3573 def ParseOrderParameters(operation, **inputParameters): 3574 """ 3575 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3576 3577 :param operation: string "Buy" or "Sell". 3578 :param inputParameters: this is dict of strings that looks like this 3579 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3580 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3581 "prices" key: one or more prices to open limit-orders 3582 Counts of values in lots and prices lists must be equals! 3583 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3584 """ 3585 # TODO: update order grid work with api v2 3586 pass 3587 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3588 # 3589 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3590 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3591 # raise Exception("Incorrect value") 3592 # 3593 # if "l" in inputParameters.keys(): 3594 # inputParameters["lots"] = inputParameters.pop("l") 3595 # 3596 # if "p" in inputParameters.keys(): 3597 # inputParameters["prices"] = inputParameters.pop("p") 3598 # 3599 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3600 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3601 # raise Exception("Incorrect value") 3602 # 3603 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3604 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3605 # 3606 # if len(lots) != len(prices): 3607 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3608 # raise Exception("Incorrect value") 3609 # 3610 # uLogger.debug("Extracted parameters for orders:") 3611 # uLogger.debug("lots = {}".format(lots)) 3612 # uLogger.debug("prices = {}".format(prices)) 3613 # 3614 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3615 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3616 # uLogger.debug("Order parameters: {}".format(result)) 3617 # 3618 # return result 3619 3620 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3621 """ 3622 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3623 3624 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3625 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3626 """ 3627 result = False 3628 msg = "Instrument not defined!" 3629 3630 if portfolio is None or not portfolio: 3631 portfolio = self.Overview(show=False) 3632 3633 if self._ticker: 3634 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3635 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3636 3637 for iType in TKS_INSTRUMENTS: 3638 for instrument in portfolio["stat"][iType]: 3639 if instrument["ticker"] == self._ticker: 3640 result = True 3641 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3642 break 3643 3644 elif self._figi: 3645 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3646 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3647 3648 for iType in TKS_INSTRUMENTS: 3649 for instrument in portfolio["stat"][iType]: 3650 if instrument["figi"] == self._figi: 3651 result = True 3652 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3653 break 3654 3655 else: 3656 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3657 3658 uLogger.debug(msg) 3659 3660 return result 3661 3662 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3663 """ 3664 Returns instrument from the user's portfolio if it presents there. 3665 Instrument must be defined by `ticker` (highly priority) or `figi`. 3666 3667 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3668 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3669 """ 3670 result = None 3671 msg = "Instrument not defined!" 3672 3673 if portfolio is None or not portfolio: 3674 portfolio = self.Overview(show=False) 3675 3676 if self._ticker: 3677 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3678 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3679 3680 for iType in TKS_INSTRUMENTS: 3681 for instrument in portfolio["stat"][iType]: 3682 if instrument["ticker"] == self._ticker: 3683 result = instrument 3684 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3685 break 3686 3687 elif self._figi: 3688 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3689 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3690 3691 for iType in TKS_INSTRUMENTS: 3692 for instrument in portfolio["stat"][iType]: 3693 if instrument["figi"] == self._figi: 3694 result = instrument 3695 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3696 break 3697 3698 else: 3699 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3700 3701 uLogger.debug(msg) 3702 3703 return result 3704 3705 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3706 """ 3707 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3708 3709 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3710 3711 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3712 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3713 """ 3714 result = False 3715 msg = "Instrument not defined!" 3716 3717 if portfolio is None or not portfolio: 3718 portfolio = self.Overview(show=False) 3719 3720 if self._ticker: 3721 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3722 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3723 3724 for instrument in portfolio["stat"]["orders"]: 3725 if instrument["ticker"] == self._ticker: 3726 result = True 3727 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3728 break 3729 3730 elif self._figi: 3731 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3732 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3733 3734 for instrument in portfolio["stat"]["orders"]: 3735 if instrument["figi"] == self._figi: 3736 result = True 3737 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3738 break 3739 3740 else: 3741 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3742 3743 uLogger.debug(msg) 3744 3745 return result 3746 3747 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3748 """ 3749 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3750 Instrument must be defined by `ticker` (highly priority) or `figi`. 3751 3752 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3753 3754 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3755 :return: list with `orderID`s of limit orders. 3756 """ 3757 result = [] 3758 msg = "Instrument not defined!" 3759 3760 if portfolio is None or not portfolio: 3761 portfolio = self.Overview(show=False) 3762 3763 if self._ticker: 3764 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3765 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3766 3767 for instrument in portfolio["stat"]["orders"]: 3768 if instrument["ticker"] == self._ticker: 3769 result.append(instrument["orderID"]) 3770 3771 if result: 3772 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3773 3774 elif self._figi: 3775 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3776 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3777 3778 for instrument in portfolio["stat"]["orders"]: 3779 if instrument["figi"] == self._figi: 3780 result.append(instrument["orderID"]) 3781 3782 if result: 3783 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3784 3785 else: 3786 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3787 3788 uLogger.debug(msg) 3789 3790 return result 3791 3792 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3793 """ 3794 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3795 3796 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3797 3798 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3799 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3800 """ 3801 result = False 3802 msg = "Instrument not defined!" 3803 3804 if portfolio is None or not portfolio: 3805 portfolio = self.Overview(show=False) 3806 3807 if self._ticker: 3808 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3809 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3810 3811 for instrument in portfolio["stat"]["stopOrders"]: 3812 if instrument["ticker"] == self._ticker: 3813 result = True 3814 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3815 break 3816 3817 elif self._figi: 3818 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3819 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3820 3821 for instrument in portfolio["stat"]["stopOrders"]: 3822 if instrument["figi"] == self._figi: 3823 result = True 3824 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3825 break 3826 3827 else: 3828 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3829 3830 uLogger.debug(msg) 3831 3832 return result 3833 3834 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3835 """ 3836 Returns list with all `orderID`s of opened stop orders for the instrument. 3837 Instrument must be defined by `ticker` (highly priority) or `figi`. 3838 3839 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3840 3841 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3842 :return: list with `orderID`s of stop orders. 3843 """ 3844 result = [] 3845 msg = "Instrument not defined!" 3846 3847 if portfolio is None or not portfolio: 3848 portfolio = self.Overview(show=False) 3849 3850 if self._ticker: 3851 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3852 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3853 3854 for instrument in portfolio["stat"]["stopOrders"]: 3855 if instrument["ticker"] == self._ticker: 3856 result.append(instrument["orderID"]) 3857 3858 if result: 3859 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3860 3861 elif self._figi: 3862 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3863 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3864 3865 for instrument in portfolio["stat"]["stopOrders"]: 3866 if instrument["figi"] == self._figi: 3867 result.append(instrument["orderID"]) 3868 3869 if result: 3870 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3871 3872 else: 3873 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3874 3875 uLogger.debug(msg) 3876 3877 return result 3878 3879 def RequestLimits(self) -> dict: 3880 """ 3881 Method for obtaining the available funds for withdrawal for current `accountId`. 3882 3883 See also: 3884 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3885 - `OverviewLimits()` method 3886 3887 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3888 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3889 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3890 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3891 """ 3892 if self.accountId is None or not self.accountId: 3893 uLogger.error("Variable `accountId` must be defined for using this method!") 3894 raise Exception("Account ID required") 3895 3896 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3897 3898 self.body = str({"accountId": self.accountId}) 3899 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3900 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3901 3902 if self.moreDebug: 3903 uLogger.debug("Records about available funds for withdrawal successfully received") 3904 3905 return rawLimits 3906 3907 def OverviewLimits(self, show: bool = False) -> dict: 3908 """ 3909 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3910 3911 See also: `RequestLimits()`. 3912 3913 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3914 :return: dict with raw parsed data from server and some calculated statistics about it. 3915 """ 3916 if self.accountId is None or not self.accountId: 3917 uLogger.error("Variable `accountId` must be defined for using this method!") 3918 raise Exception("Account ID required") 3919 3920 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3921 3922 view = { 3923 "rawLimits": rawLimits, 3924 "limits": { # parsed data for every currency: 3925 "money": { # this is an array of portfolio currency positions 3926 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3927 }, 3928 "blocked": { # this is an array of blocked currency 3929 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3930 }, 3931 "blockedGuarantee": { # this is locked money under collateral for futures 3932 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3933 }, 3934 }, 3935 } 3936 3937 # --- Prepare text table with limits in human-readable format: 3938 if show: 3939 info = [ 3940 "# Withdrawal limits\n\n", 3941 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3942 "* **Account ID:** [{}]\n".format(self.accountId), 3943 ] 3944 3945 if view["limits"]["money"]: 3946 info.extend([ 3947 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3948 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3949 ]) 3950 3951 else: 3952 info.append("\nNo withdrawal limits\n") 3953 3954 for curr in view["limits"]["money"].keys(): 3955 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3956 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3957 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3958 3959 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3960 "[{}]".format(curr), 3961 "{:.2f}".format(view["limits"]["money"][curr]), 3962 "{:.2f}".format(availableMoney), 3963 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3964 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3965 ) 3966 3967 if curr == "rub": 3968 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3969 3970 else: 3971 info.append(infoStr) 3972 3973 infoText = "".join(info) 3974 3975 uLogger.info(infoText) 3976 3977 if self.withdrawalLimitsFile: 3978 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3979 fH.write(infoText) 3980 3981 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3982 3983 if self.useHTMLReports: 3984 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3985 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3986 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3987 3988 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3989 3990 return view 3991 3992 def RequestAccounts(self) -> dict: 3993 """ 3994 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3995 3996 See also: 3997 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3998 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3999 - `OverviewUserInfo()` method 4000 4001 :return: dict with raw data from server that contains accounts info. Example of dict: 4002 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4003 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4004 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4005 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4006 """ 4007 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4008 4009 self.body = str({}) 4010 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4011 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4012 4013 if self.moreDebug: 4014 uLogger.debug("Records about available accounts successfully received") 4015 4016 return rawAccounts 4017 4018 def RequestUserInfo(self) -> dict: 4019 """ 4020 Method for requesting common user's information. 4021 4022 See also: 4023 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4024 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4025 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4026 - `OverviewUserInfo()` method 4027 4028 :return: dict with raw data from server that contains user's information. Example of dict: 4029 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4030 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4031 """ 4032 uLogger.debug("Requesting common user's information. Wait, please...") 4033 4034 self.body = str({}) 4035 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4036 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4037 4038 if self.moreDebug: 4039 uLogger.debug("Records about current user successfully received") 4040 4041 return rawUserInfo 4042 4043 def RequestMarginStatus(self, accountId: str = None) -> dict: 4044 """ 4045 Method for requesting margin calculation for defined account ID. 4046 4047 See also: 4048 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4049 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4050 - `OverviewUserInfo()` method 4051 4052 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4053 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4054 Example of responses: 4055 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4056 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4057 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4058 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4059 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4060 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4061 """ 4062 if accountId is None or not accountId: 4063 if self.accountId is None or not self.accountId: 4064 uLogger.error("Variable `accountId` must be defined for using this method!") 4065 raise Exception("Account ID required") 4066 4067 else: 4068 accountId = self.accountId # use `self.accountId` (main ID) by default 4069 4070 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4071 4072 self.body = str({"accountId": accountId}) 4073 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4074 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4075 4076 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4077 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4078 rawMargin = {} 4079 4080 else: 4081 if self.moreDebug: 4082 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4083 4084 return rawMargin 4085 4086 def RequestTariffLimits(self) -> dict: 4087 """ 4088 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4089 4090 See also: 4091 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4092 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4093 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4094 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4095 - `OverviewUserInfo()` method 4096 4097 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4098 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4099 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4100 """ 4101 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4102 4103 self.body = str({}) 4104 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4105 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4106 4107 if self.moreDebug: 4108 uLogger.debug("Records with limits of current tariff successfully received") 4109 4110 return rawTariffLimits 4111 4112 def RequestBondCoupons(self, iJSON: dict) -> dict: 4113 """ 4114 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4115 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4116 All dates are in UTC timezone. 4117 4118 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4119 Documentation: 4120 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4121 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4122 4123 See also: `ExtendBondsData()`. 4124 4125 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4126 If raw iJSON is not data of bond then server returns an error [400] with message: 4127 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4128 :return: dictionary with bond payment calendar. Response example 4129 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4130 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4131 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4132 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4133 """ 4134 if iJSON["figi"] is None or not iJSON["figi"]: 4135 uLogger.error("FIGI must be defined for using this method!") 4136 raise Exception("FIGI required") 4137 4138 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4139 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4140 4141 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4142 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4143 self._figi, 4144 startDate, 4145 endDate, 4146 )) 4147 4148 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4149 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4150 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4151 4152 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4153 uLogger.warning("Instrument type is not bond!") 4154 4155 else: 4156 if self.moreDebug: 4157 uLogger.debug("Records about bond payment calendar successfully received") 4158 4159 return calendar 4160 4161 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4162 """ 4163 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4164 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4165 coupon yields, current yields and some statistics etc. 4166 4167 WARNING! This is too long operation if a lot of bonds requested from broker server. 4168 4169 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4170 4171 :param instruments: list of strings with tickers or FIGIs. 4172 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4173 for further used by data scientists or stock analytics. 4174 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4175 In XLSX-file and Pandas DataFrame fields mean: 4176 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4177 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4178 """ 4179 if instruments is None or not instruments: 4180 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4181 raise Exception("Ticker or FIGI required") 4182 4183 if isinstance(instruments, str): 4184 instruments = [instruments] 4185 4186 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4187 4188 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4189 4190 iCount = len(uniqueInstruments) 4191 tooLong = iCount >= 20 4192 if tooLong: 4193 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4194 4195 bonds = None 4196 for i, self._figi in enumerate(uniqueInstruments): 4197 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4198 4199 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4200 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4201 rawBond = self.SearchByFIGI(requestPrice=True) 4202 4203 # Widen raw data with UTC current time (iData["actualDateTime"]): 4204 actualDate = datetime.now(tzutc()) 4205 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4206 4207 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4208 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4209 4210 # Replace some values with human-readable: 4211 iData["nominalCurrency"] = iData["nominal"]["currency"] 4212 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4213 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4214 iData["aciCurrency"] = iData["aciValue"]["currency"] 4215 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4216 iData["issueSize"] = int(iData["issueSize"]) 4217 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4218 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4219 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4220 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4221 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4222 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4223 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4224 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4225 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4226 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4227 4228 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4229 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4230 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4231 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4232 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4233 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4234 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4235 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4236 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4237 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4238 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4239 4240 # Widen raw data with calendar data from `rawCalendar` values: 4241 calendarData = [] 4242 if "events" in iData["rawCalendar"].keys(): 4243 for item in iData["rawCalendar"]["events"]: 4244 calendarData.append({ 4245 "couponDate": item["couponDate"], 4246 "couponNumber": int(item["couponNumber"]), 4247 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4248 "payCurrency": item["payOneBond"]["currency"], 4249 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4250 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4251 "couponStartDate": item["couponStartDate"], 4252 "couponEndDate": item["couponEndDate"], 4253 "couponPeriod": item["couponPeriod"], 4254 }) 4255 4256 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4257 if "maturityDate" not in iData.keys(): 4258 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4259 4260 # Widen raw data with Coupon Rate. 4261 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4262 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4263 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4264 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4265 4266 # Widen raw data with Yield to Maturity (YTM) on current date. 4267 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4268 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4269 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4270 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4271 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4272 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4273 4274 iData["calendar"] = calendarData # adds calendar at the end 4275 4276 # Remove not used data: 4277 iData.pop("uid") 4278 iData.pop("positionUid") 4279 iData.pop("currentPrice") 4280 iData.pop("rawCalendar") 4281 4282 colNames = list(iData.keys()) 4283 if bonds is None: 4284 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4285 4286 else: 4287 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4288 4289 else: 4290 uLogger.warning("Instrument is not a bond!") 4291 4292 processed = round(100 * (i + 1) / iCount, 1) 4293 if tooLong and processed % 5 == 0: 4294 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4295 4296 else: 4297 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4298 4299 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4300 4301 # Saving bonds from Pandas DataFrame to XLSX sheet: 4302 if xlsx and self.bondsXLSXFile: 4303 with pd.ExcelWriter( 4304 path=self.bondsXLSXFile, 4305 date_format=TKS_DATE_FORMAT, 4306 datetime_format=TKS_DATE_TIME_FORMAT, 4307 mode="w", 4308 ) as writer: 4309 bonds.to_excel( 4310 writer, 4311 sheet_name="Extended bonds data", 4312 index=True, 4313 encoding="UTF-8", 4314 freeze_panes=(1, 1), 4315 ) # saving as XLSX-file with freeze first row and column as headers 4316 4317 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4318 4319 return bonds 4320 4321 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4322 """ 4323 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4324 4325 WARNING! This is too long operation if a lot of bonds requested from broker server. 4326 4327 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4328 4329 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4330 extended information about bonds: main info, current prices, bond payment calendar, 4331 coupon yields, current yields and some statistics etc. 4332 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4333 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4334 for further used by data scientists or stock analytics. 4335 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4336 """ 4337 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4338 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4339 4340 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4341 4342 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4343 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4344 calendar = None 4345 for bond in extBonds.iterrows(): 4346 for item in bond[1]["calendar"]: 4347 cData = { 4348 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4349 "couponDate": item["couponDate"], 4350 "figi": bond[1]["figi"], 4351 "ticker": bond[1]["ticker"], 4352 "name": bond[1]["name"], 4353 "couponNumber": item["couponNumber"], 4354 "payOneBond": item["payOneBond"], 4355 "payCurrency": item["payCurrency"], 4356 "couponType": item["couponType"], 4357 "couponPeriod": item["couponPeriod"], 4358 "fixDate": item["fixDate"], 4359 "couponStartDate": item["couponStartDate"], 4360 "couponEndDate": item["couponEndDate"], 4361 } 4362 4363 if calendar is None: 4364 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4365 4366 else: 4367 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4368 4369 if calendar is not None: 4370 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4371 4372 # Saving calendar from Pandas DataFrame to XLSX sheet: 4373 if xlsx: 4374 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4375 4376 with pd.ExcelWriter( 4377 path=xlsxCalendarFile, 4378 date_format=TKS_DATE_FORMAT, 4379 datetime_format=TKS_DATE_TIME_FORMAT, 4380 mode="w", 4381 ) as writer: 4382 humanReadable = calendar.copy(deep=True) 4383 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4384 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4385 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4386 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4387 humanReadable.columns = colNames # human-readable column names 4388 4389 humanReadable.to_excel( 4390 writer, 4391 sheet_name="Bond payments calendar", 4392 index=False, 4393 encoding="UTF-8", 4394 freeze_panes=(1, 2), 4395 ) # saving as XLSX-file with freeze first row and column as headers 4396 4397 del humanReadable # release df in memory 4398 4399 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4400 4401 return calendar 4402 4403 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4404 """ 4405 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4406 Also, creates Markdown file with calendar data, `calendar.md` by default. 4407 4408 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4409 4410 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4411 extended information about bonds: main info, current prices, bond payment calendar, 4412 coupon yields, current yields and some statistics etc. 4413 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4414 :param show: if `True` then also printing bonds payment calendar to the console, 4415 otherwise save to file `calendarFile` only. `False` by default. 4416 :return: multilines text in Markdown format with bonds payment calendar as a table. 4417 """ 4418 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4419 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4420 4421 infoText = "# Bond payments calendar\n\n" 4422 4423 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4424 4425 if not (calendar is None or calendar.empty): 4426 splitLine = "| | | | | | | | | |\n" 4427 4428 info = [ 4429 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4430 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4431 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4432 ] 4433 4434 newMonth = False 4435 notOneBond = calendar["figi"].nunique() > 1 4436 for i, bond in enumerate(calendar.iterrows()): 4437 if newMonth and notOneBond: 4438 info.append(splitLine) 4439 4440 info.append( 4441 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4442 " √" if bond[1]["paid"] else " —", 4443 bond[1]["couponDate"].split("T")[0], 4444 bond[1]["figi"], 4445 bond[1]["ticker"], 4446 bond[1]["couponNumber"], 4447 "{} {}".format( 4448 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4449 bond[1]["payCurrency"], 4450 ), 4451 bond[1]["couponType"], 4452 bond[1]["couponPeriod"], 4453 bond[1]["fixDate"].split("T")[0], 4454 ) 4455 ) 4456 4457 if i < len(calendar.values) - 1: 4458 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4459 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4460 newMonth = False if curDate.month == nextDate.month else True 4461 4462 else: 4463 newMonth = False 4464 4465 infoText += "".join(info) 4466 4467 if show: 4468 uLogger.info("{}".format(infoText)) 4469 4470 if self.calendarFile is not None: 4471 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4472 fH.write(infoText) 4473 4474 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4475 4476 if self.useHTMLReports: 4477 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4478 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4479 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4480 4481 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4482 4483 else: 4484 infoText += "No data\n" 4485 4486 return infoText 4487 4488 def OverviewAccounts(self, show: bool = False) -> dict: 4489 """ 4490 Method for parsing and show simple table with all available user accounts. 4491 4492 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4493 4494 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4495 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4496 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4497 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4498 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4499 "closed": "—", "access": "Full access" }, ...}}` 4500 """ 4501 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4502 4503 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4504 accounts = { 4505 item["id"]: { 4506 "type": TKS_ACCOUNT_TYPES[item["type"]], 4507 "name": item["name"], 4508 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4509 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4510 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4511 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4512 } for item in rawAccounts["accounts"] 4513 } 4514 4515 # Raw and parsed data with some fields replaced in "stat" section: 4516 view = { 4517 "rawAccounts": rawAccounts, 4518 "stat": accounts, 4519 } 4520 4521 # --- Prepare simple text table with only accounts data in human-readable format: 4522 if show: 4523 info = [ 4524 "# User accounts\n\n", 4525 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4526 "| Account ID | Type | Status | Name |\n", 4527 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4528 ] 4529 4530 for account in view["stat"].keys(): 4531 info.extend([ 4532 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4533 account, 4534 view["stat"][account]["type"], 4535 view["stat"][account]["status"], 4536 view["stat"][account]["name"], 4537 ) 4538 ]) 4539 4540 infoText = "".join(info) 4541 4542 uLogger.info(infoText) 4543 4544 if self.userAccountsFile: 4545 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4546 fH.write(infoText) 4547 4548 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4549 4550 if self.useHTMLReports: 4551 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4552 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4553 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4554 4555 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4556 4557 return view 4558 4559 def OverviewUserInfo(self, show: bool = False) -> dict: 4560 """ 4561 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4562 4563 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4564 4565 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4566 :return: dict with raw parsed data from server and some calculated statistics about it. 4567 """ 4568 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4569 tmpTicker = self._ticker 4570 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4571 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4572 self._ticker = tmpTicker 4573 4574 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4575 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4576 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4577 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4578 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4579 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4580 4581 # This is dict with parsed common user data: 4582 userInfo = { 4583 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4584 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4585 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4586 "tariff": rawUserInfo["tariff"], 4587 } 4588 4589 # This is an array of dict with parsed margin statuses for every account IDs: 4590 margins = {} 4591 for accountId in accounts.keys(): 4592 if rawMargins[accountId]: 4593 margins[accountId] = { 4594 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4595 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4596 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4597 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4598 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4599 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4600 "missing": missing["volume"], 4601 } 4602 4603 else: 4604 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4605 4606 unary = {} # unary-connection limits 4607 for item in rawTariffLimits["unaryLimits"]: 4608 if item["limitPerMinute"] in unary.keys(): 4609 unary[item["limitPerMinute"]].extend(item["methods"]) 4610 4611 else: 4612 unary[item["limitPerMinute"]] = item["methods"] 4613 4614 stream = {} # stream-connection limits 4615 for item in rawTariffLimits["streamLimits"]: 4616 if item["limit"] in stream.keys(): 4617 stream[item["limit"]].extend(item["streams"]) 4618 4619 else: 4620 stream[item["limit"]] = item["streams"] 4621 4622 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4623 limits = { 4624 "unary": unary, 4625 "stream": stream, 4626 } 4627 4628 # Raw and parsed data as an output result: 4629 view = { 4630 "rawUserInfo": rawUserInfo, 4631 "rawAccounts": rawAccounts, 4632 "rawMargins": rawMargins, 4633 "rawTariffLimits": rawTariffLimits, 4634 "stat": { 4635 "overview": overview, 4636 "userInfo": userInfo, 4637 "accounts": accounts, 4638 "margins": margins, 4639 "limits": limits, 4640 }, 4641 } 4642 4643 # --- Prepare text table with user information in human-readable format: 4644 if show: 4645 info = [ 4646 "# Full user information\n\n", 4647 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4648 "## Common information\n\n", 4649 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4650 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4651 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4652 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4653 "\n## User accounts\n\n", 4654 ] 4655 4656 for account in view["stat"]["accounts"].keys(): 4657 info.extend([ 4658 "### ID: [{}]\n\n".format(account), 4659 "| Parameters | Values |\n", 4660 "|----------------------|--------------------------------------------------------------|\n", 4661 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4662 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4663 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4664 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4665 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4666 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4667 ]) 4668 4669 if margins[account]: 4670 info.extend([ 4671 "| Margin status: | Enabled |\n", 4672 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4673 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4674 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4675 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4676 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4677 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4678 ]) 4679 4680 else: 4681 info.append("| Margin status: | Disabled |\n\n") 4682 4683 info.extend([ 4684 "\n## Current user tariff limits\n", 4685 "\n### See also\n", 4686 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4687 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4688 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4689 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4690 "\n### Unary limits\n", 4691 ]) 4692 4693 if unary: 4694 for key, values in sorted(unary.items()): 4695 info.append("\n* Max requests per minute: {}\n".format(key)) 4696 4697 for value in values: 4698 info.append(" - {}\n".format(value)) 4699 4700 else: 4701 info.append("\nNot available\n") 4702 4703 info.append("\n### Stream limits\n") 4704 4705 if stream: 4706 for key, values in sorted(stream.items()): 4707 info.append("\n* Max stream connections: {}\n".format(key)) 4708 4709 for value in values: 4710 info.append(" - {}\n".format(value)) 4711 4712 else: 4713 info.append("\nNot available\n") 4714 4715 infoText = "".join(info) 4716 4717 uLogger.info(infoText) 4718 4719 if self.userInfoFile: 4720 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4721 fH.write(infoText) 4722 4723 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4724 4725 if self.useHTMLReports: 4726 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4727 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4728 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4729 4730 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4731 4732 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self.__lock = Lock() # initialize multiprocessing mutex lock 130 131 self.aliases = TKS_TICKER_ALIASES 132 """Some aliases instead official tickers. 133 134 See also: `TKSEnums.TKS_TICKER_ALIASES` 135 """ 136 137 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 138 139 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 140 141 self._ticker = "" 142 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 143 144 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 145 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 146 147 See also: `SearchByTicker()`, `SearchInstruments()`. 148 """ 149 150 self._figi = "" 151 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 152 153 See also: `SearchByFIGI()`, `SearchInstruments()`. 154 """ 155 156 self.depth = 1 157 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 158 159 See also: `GetCurrentPrices()`. 160 """ 161 162 self.server = r"https://invest-public-api.tinkoff.ru/rest" 163 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 164 165 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 166 """ 167 168 uLogger.debug("Broker API server: {}".format(self.server)) 169 170 self.timeout = 15 171 """Server operations timeout in seconds. Default: `15`. 172 173 See also: `SendAPIRequest()`. 174 """ 175 176 self.headers = { 177 "Content-Type": "application/json", 178 "accept": "application/json", 179 "Authorization": "Bearer {}".format(self.token), 180 "x-app-name": "Tim55667757.TKSBrokerAPI", 181 } 182 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 183 184 See also: `SendAPIRequest()`. 185 """ 186 187 self.body = None 188 """Request body which send to broker server. Default: `None`. 189 190 See also: `SendAPIRequest()`. 191 """ 192 193 self.moreDebug = False 194 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 195 196 self.useHTMLReports = False 197 """ 198 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 199 200 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 201 """ 202 203 self.historyFile = None 204 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 205 206 See also: `History()`. 207 """ 208 209 self.htmlHistoryFile = "index.html" 210 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 211 212 See also: `ShowHistoryChart()`. 213 """ 214 215 self.instrumentsFile = "instruments.md" 216 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 217 218 See also: `ShowInstrumentsInfo()`. 219 """ 220 221 self.searchResultsFile = "search-results.md" 222 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 223 224 See also: `SearchInstruments()`. 225 """ 226 227 self.pricesFile = "prices.md" 228 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 229 230 See also: `GetListOfPrices()`. 231 """ 232 233 self.infoFile = "info.md" 234 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 235 236 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 237 """ 238 239 self.bondsXLSXFile = "ext-bonds.xlsx" 240 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 241 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 242 243 See also: `ExtendBondsData()`. 244 """ 245 246 self.calendarFile = "calendar.md" 247 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 248 249 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 250 251 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 252 """ 253 254 self.overviewFile = "overview.md" 255 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 256 257 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 258 """ 259 260 self.overviewDigestFile = "overview-digest.md" 261 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 262 263 See also: `Overview()` with parameter `details="digest"`. 264 """ 265 266 self.overviewPositionsFile = "overview-positions.md" 267 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 268 269 See also: `Overview()` with parameter `details="positions"`. 270 """ 271 272 self.overviewOrdersFile = "overview-orders.md" 273 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 274 275 See also: `Overview()` with parameter `details="orders"`. 276 """ 277 278 self.overviewAnalyticsFile = "overview-analytics.md" 279 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 280 281 See also: `Overview()` with parameter `details="analytics"`. 282 """ 283 284 self.overviewBondsCalendarFile = "overview-calendar.md" 285 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 286 287 See also: `Overview()` with parameter `details="calendar"`. 288 """ 289 290 self.reportFile = "deals.md" 291 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 292 293 See also: `Deals()`. 294 """ 295 296 self.withdrawalLimitsFile = "limits.md" 297 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 298 299 See also: `OverviewLimits()` and `RequestLimits()`. 300 """ 301 302 self.userInfoFile = "user-info.md" 303 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 304 305 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 306 """ 307 308 self.userAccountsFile = "accounts.md" 309 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 310 311 See also: `OverviewAccounts()`, `RequestAccounts()`. 312 """ 313 314 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 315 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 316 317 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 318 319 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 320 """ 321 322 self.iList = None # init iList for raw instruments data 323 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 324 325 See also: `Listing()`, `DumpInstruments()`. 326 """ 327 328 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 329 if useCache: 330 if os.path.exists(self.iListDumpFile): 331 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 332 curTime = datetime.now(tzutc()) 333 334 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 335 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 336 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 338 339 else: 340 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 341 342 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 343 os.path.abspath(self.iListDumpFile), 344 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 345 )) 346 347 else: 348 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 349 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 350 351 else: 352 self.iList = self.Listing() # request new raw instruments data from broker server 353 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 354 355 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 356 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 357 358 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 359 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.
See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
418 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 419 """ 420 Send GET or POST request to broker server and receive JSON object. 421 422 self.header: must be defining with dictionary of headers. 423 self.body: if define then used as request body. None by default. 424 self.timeout: global request timeout, 15 seconds by default. 425 :param url: url with REST request. 426 :param reqType: send "GET" or "POST" request. "GET" by default. 427 :param retry: how many times retry after first request if an 5xx server errors occurred. 428 :param pause: sleep time in seconds between retries. 429 :return: response JSON (dictionary) from broker. 430 """ 431 if reqType.upper() not in ("GET", "POST"): 432 uLogger.error("You can define request type: `GET` or `POST`!") 433 raise Exception("Incorrect value") 434 435 if self.moreDebug: 436 uLogger.debug("Request parameters:") 437 uLogger.debug(" - REST API URL: {}".format(url)) 438 uLogger.debug(" - request type: {}".format(reqType)) 439 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 440 uLogger.debug(" - body:\n{}".format(self.body)) 441 442 # fast hack to avoid all operations with some tickers/FIGI 443 responseJSON = {} 444 oK = True 445 for item in self.exclude: 446 if item in url: 447 if self.moreDebug: 448 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 449 450 oK = False 451 break 452 453 if oK: 454 with self.__lock: # acquire the mutex lock 455 counter = 0 456 response = None 457 errMsg = "" 458 459 while not response and counter <= retry: 460 if reqType == "GET": 461 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 462 463 if reqType == "POST": 464 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 465 466 if self.moreDebug: 467 uLogger.debug("Response:") 468 uLogger.debug(" - status code: {}".format(response.status_code)) 469 uLogger.debug(" - reason: {}".format(response.reason)) 470 uLogger.debug(" - body length: {}".format(len(response.text))) 471 uLogger.debug(" - headers:\n{}".format(response.headers)) 472 473 # Server returns some headers: 474 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 475 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 476 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 477 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 478 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 479 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 480 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 481 sleep(rateLimitWait) 482 483 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 484 if 400 <= response.status_code < 500: 485 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 486 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 487 488 if "code" in response.text and "message" in response.text: 489 msgDict = self._ParseJSON(rawData=response.text) 490 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 491 492 counter = retry + 1 # do not retry for 4xx errors 493 494 if 500 <= response.status_code < 600: 495 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 496 uLogger.debug(" - not oK, {}".format(errMsg)) 497 498 if "code" in response.text and "message" in response.text: 499 errMsgDict = self._ParseJSON(rawData=response.text) 500 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 501 502 counter += 1 503 504 if counter <= retry: 505 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 506 sleep(pause) 507 508 responseJSON = self._ParseJSON(rawData=response.text) 509 510 if errMsg: 511 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 512 uLogger.error(" - not oK, {}".format(errMsg)) 513 514 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
547 def Listing(self) -> dict: 548 """ 549 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 550 551 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 552 """ 553 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 554 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 555 556 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 557 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 558 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 559 560 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 561 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 562 poolUpdater.close() # close the thread pool 563 poolUpdater.join() # wait a moment until all data returns from threads 564 565 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 566 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 567 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 568 569 # calculate minimum price increment (step) for all instruments and set up instrument's type: 570 for iType in iList.keys(): 571 for ticker in iList[iType]: 572 iList[iType][ticker]["type"] = iType 573 574 if "minPriceIncrement" in iList[iType][ticker].keys(): 575 iList[iType][ticker]["step"] = NanoToFloat( 576 iList[iType][ticker]["minPriceIncrement"]["units"], 577 iList[iType][ticker]["minPriceIncrement"]["nano"], 578 ) 579 580 else: 581 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 582 583 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
585 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 586 """ 587 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 588 589 See also: `DumpInstruments()`, `Listing()`. 590 591 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 592 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 593 """ 594 if self.iListDumpFile is None or not self.iListDumpFile: 595 uLogger.error("Output name of dump file must be defined!") 596 raise Exception("Filename required") 597 598 if not self.iList or forceUpdate: 599 self.iList = self.Listing() 600 601 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 602 603 # Save as XLSX with separated sheets for every type of instruments: 604 with pd.ExcelWriter( 605 path=xlsxDumpFile, 606 date_format=TKS_DATE_FORMAT, 607 datetime_format=TKS_DATE_TIME_FORMAT, 608 mode="w", 609 ) as writer: 610 for iType in TKS_INSTRUMENTS: 611 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 612 df = df[sorted(df)] # sorted by column names 613 df = df.applymap( 614 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 615 na_action="ignore", 616 ) # converting numbers from nano-type to float in every cell 617 df.to_excel( 618 writer, 619 sheet_name=iType, 620 encoding="UTF-8", 621 freeze_panes=(1, 1), 622 ) # saving as XLSX-file with freeze first row and column as headers 623 624 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
626 def DumpInstruments(self, forceUpdate: bool = True) -> str: 627 """ 628 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 629 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 630 631 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 632 633 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 634 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 635 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 636 """ 637 if self.iListDumpFile is None or not self.iListDumpFile: 638 uLogger.error("Output name of dump file must be defined!") 639 raise Exception("Filename required") 640 641 if not self.iList or forceUpdate: 642 self.iList = self.Listing() 643 644 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 645 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 646 fH.write(jsonDump) 647 648 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 649 650 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
652 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 653 """ 654 Show information about one instrument defined by json data and prints it in Markdown format. 655 656 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 657 658 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 659 :param show: if `True` then also printing information about instrument and its current price. 660 :return: multilines text in Markdown format with information about one instrument. 661 """ 662 splitLine = "| | |\n" 663 infoText = "" 664 665 if iJSON is not None and iJSON and isinstance(iJSON, dict): 666 info = [ 667 "# Main information\n\n", 668 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 669 "| Parameters | Values |\n", 670 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 671 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 672 "| Full name: | {:<54} |\n".format(iJSON["name"]), 673 ] 674 675 if "sector" in iJSON.keys() and iJSON["sector"]: 676 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 677 678 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 679 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 680 681 info.extend([ 682 splitLine, 683 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 684 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 685 ]) 686 687 if "isin" in iJSON.keys() and iJSON["isin"]: 688 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 689 690 if "classCode" in iJSON.keys(): 691 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 692 693 info.extend([ 694 splitLine, 695 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 696 splitLine, 697 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 698 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 699 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 700 ]) 701 702 if iJSON["figi"]: 703 self._figi = iJSON["figi"] 704 iJSON = iJSON | self.RequestTradingStatus() 705 706 info.extend([ 707 splitLine, 708 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 709 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 710 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 711 ]) 712 713 info.append(splitLine) 714 715 if "type" in iJSON.keys() and iJSON["type"]: 716 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 717 718 if "shareType" in iJSON.keys() and iJSON["shareType"]: 719 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 720 721 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 722 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 723 724 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 725 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 726 727 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 728 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 729 730 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 731 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 732 733 if "focusType" in iJSON.keys() and iJSON["focusType"]: 734 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 735 736 if "assetType" in iJSON.keys() and iJSON["assetType"]: 737 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 738 739 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 740 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 741 742 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 743 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 744 745 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 746 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 747 748 if "currency" in iJSON.keys(): 749 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 750 751 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 752 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 753 754 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 755 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 756 757 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 758 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 759 760 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 761 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 762 763 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 764 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 765 766 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 767 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 768 769 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 770 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 771 772 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 773 info.append("| Perpetual bond: | Yes |\n") 774 775 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 776 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 777 778 iExt = None 779 if iJSON["type"] == "Bonds": 780 info.extend([ 781 splitLine, 782 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 783 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 784 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 785 iJSON["nominal"]["currency"], 786 )), 787 ]) 788 789 if "floatingCouponFlag" in iJSON.keys(): 790 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 791 792 if "amortizationFlag" in iJSON.keys(): 793 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 794 795 info.append(splitLine) 796 797 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 798 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 799 800 if iJSON["figi"]: 801 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 802 803 info.extend([ 804 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 805 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 806 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 807 ]) 808 809 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 810 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 811 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 812 iJSON["aciValue"]["currency"] 813 ))) 814 815 if "currentPrice" in iJSON.keys(): 816 info.append(splitLine) 817 818 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 819 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 820 821 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 822 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 823 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 824 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 825 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 826 827 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 828 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 829 830 info.extend([ 831 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 832 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 833 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 834 )), 835 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 836 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 837 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 838 )), 839 "| Changes between last deal price and last close | {:<54} |\n".format( 840 "{:.2f}%{}".format( 841 iJSON["currentPrice"]["changes"], 842 " ({}{:.2f} {})".format( 843 "+" if bondChangesDelta > 0 else "", 844 bondChangesDelta, 845 aciCurrency 846 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 847 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 848 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 849 currency 850 ), 851 ) 852 ), 853 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 854 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 855 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 856 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 857 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 858 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 859 )), 860 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 861 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 862 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 863 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 864 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 865 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 866 )), 867 ]) 868 869 if "lot" in iJSON.keys(): 870 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 871 872 if "step" in iJSON.keys() and iJSON["step"] != 0: 873 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 874 875 # Add bond payment calendar: 876 if iJSON["type"] == "Bonds": 877 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 878 info.extend(["\n#", strCalendar]) 879 880 infoText += "".join(info) 881 882 if show: 883 uLogger.info("{}".format(infoText)) 884 885 else: 886 uLogger.debug("{}".format(infoText)) 887 888 if self.infoFile is not None: 889 with open(self.infoFile, "w", encoding="UTF-8") as fH: 890 fH.write(infoText) 891 892 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 893 894 if self.useHTMLReports: 895 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 896 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 897 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 898 899 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 900 901 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self._ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
903 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 904 """ 905 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 906 907 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 908 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 909 :return: JSON formatted data with information about instrument. 910 """ 911 tickerJSON = {} 912 if self.moreDebug: 913 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 914 915 if not self._ticker: 916 uLogger.warning("self._ticker variable is not be empty!") 917 918 else: 919 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 920 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 921 raise Exception("Instrument not allowed") 922 923 if not self.iList: 924 self.iList = self.Listing() 925 926 if self._ticker in self.iList["Shares"].keys(): 927 tickerJSON = self.iList["Shares"][self._ticker] 928 if self.moreDebug: 929 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 930 931 elif self._ticker in self.iList["Currencies"].keys(): 932 tickerJSON = self.iList["Currencies"][self._ticker] 933 if self.moreDebug: 934 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 935 936 elif self._ticker in self.iList["Bonds"].keys(): 937 tickerJSON = self.iList["Bonds"][self._ticker] 938 if self.moreDebug: 939 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 940 941 elif self._ticker in self.iList["Etfs"].keys(): 942 tickerJSON = self.iList["Etfs"][self._ticker] 943 if self.moreDebug: 944 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 945 946 elif self._ticker in self.iList["Futures"].keys(): 947 tickerJSON = self.iList["Futures"][self._ticker] 948 if self.moreDebug: 949 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 950 951 if tickerJSON: 952 self._figi = tickerJSON["figi"] 953 954 if requestPrice: 955 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 956 957 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 958 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 959 960 else: 961 tickerJSON["currentPrice"]["changes"] = 0 962 963 if show: 964 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 965 966 else: 967 if show: 968 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 969 970 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
972 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 973 """ 974 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 975 976 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 977 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 978 :return: JSON formatted data with information about instrument. 979 """ 980 figiJSON = {} 981 if self.moreDebug: 982 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 983 984 if not self._figi: 985 uLogger.warning("self._figi variable is not be empty!") 986 987 else: 988 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 989 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 990 raise Exception("Instrument not allowed") 991 992 if not self.iList: 993 self.iList = self.Listing() 994 995 for item in self.iList["Shares"].keys(): 996 if self._figi == self.iList["Shares"][item]["figi"]: 997 figiJSON = self.iList["Shares"][item] 998 999 if self.moreDebug: 1000 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1001 1002 break 1003 1004 if not figiJSON: 1005 for item in self.iList["Currencies"].keys(): 1006 if self._figi == self.iList["Currencies"][item]["figi"]: 1007 figiJSON = self.iList["Currencies"][item] 1008 1009 if self.moreDebug: 1010 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1011 1012 break 1013 1014 if not figiJSON: 1015 for item in self.iList["Bonds"].keys(): 1016 if self._figi == self.iList["Bonds"][item]["figi"]: 1017 figiJSON = self.iList["Bonds"][item] 1018 1019 if self.moreDebug: 1020 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1021 1022 break 1023 1024 if not figiJSON: 1025 for item in self.iList["Etfs"].keys(): 1026 if self._figi == self.iList["Etfs"][item]["figi"]: 1027 figiJSON = self.iList["Etfs"][item] 1028 1029 if self.moreDebug: 1030 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1031 1032 break 1033 1034 if not figiJSON: 1035 for item in self.iList["Futures"].keys(): 1036 if self._figi == self.iList["Futures"][item]["figi"]: 1037 figiJSON = self.iList["Futures"][item] 1038 1039 if self.moreDebug: 1040 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1041 1042 break 1043 1044 if figiJSON: 1045 self._figi = figiJSON["figi"] 1046 self._ticker = figiJSON["ticker"] 1047 1048 if requestPrice: 1049 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1050 1051 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1052 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1053 1054 else: 1055 figiJSON["currentPrice"]["changes"] = 0 1056 1057 if show: 1058 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1059 1060 else: 1061 if show: 1062 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1063 1064 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1066 def GetCurrentPrices(self, show: bool = True) -> dict: 1067 """ 1068 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1069 `{"buy": [{"price": 1243.8, "quantity": 193}, 1070 {"price": 1244.0, "quantity": 168}, 1071 {"price": 1244.8, "quantity": 5}, 1072 {"price": 1245.0, "quantity": 61}, 1073 {"price": 1245.4, "quantity": 60}], 1074 "sell": [{"price": 1243.6, "quantity": 8}, 1075 {"price": 1242.6, "quantity": 10}, 1076 {"price": 1242.4, "quantity": 18}, 1077 {"price": 1242.2, "quantity": 50}, 1078 {"price": 1242.0, "quantity": 113}], 1079 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1080 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1081 - sell: list of dicts with Buyers prices, 1082 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1083 - quantity: volume value by current price in lots, 1084 - limitUp: current trade session limit price, maximum, 1085 - limitDown: current trade session limit price, minimum, 1086 - lastPrice: last deal price of the instrument, 1087 - closePrice: previous trade session close price of the instrument. 1088 1089 See also: `SearchByTicker()` and `SearchByFIGI()`. 1090 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1091 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1092 1093 :param show: if `True` then print DOM to log and console. 1094 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1095 If an error occurred then returns an empty record: 1096 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1097 """ 1098 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1099 1100 if self.depth < 1: 1101 uLogger.error("Depth of Market (DOM) must be >=1!") 1102 raise Exception("Incorrect value") 1103 1104 if not (self._ticker or self._figi): 1105 uLogger.error("self._ticker or self._figi variables must be defined!") 1106 raise Exception("Ticker or FIGI required") 1107 1108 if self._ticker and not self._figi: 1109 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1110 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1111 1112 if not self._ticker and self._figi: 1113 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1114 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1115 1116 if not self._figi: 1117 uLogger.error("FIGI is not defined!") 1118 raise Exception("Ticker or FIGI required") 1119 1120 else: 1121 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1122 1123 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1124 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1125 self.body = str({"figi": self._figi, "depth": self.depth}) 1126 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1127 1128 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1129 # list of dicts with sellers orders: 1130 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1131 1132 # list of dicts with buyers orders: 1133 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1134 1135 # max price of instrument at this time: 1136 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1137 1138 # min price of instrument at this time: 1139 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1140 1141 # last price of deal with instrument: 1142 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1143 1144 # last close price of instrument: 1145 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1146 1147 else: 1148 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1149 uLogger.debug("Server response: {}".format(pricesResponse)) 1150 1151 if show: 1152 if prices["buy"] or prices["sell"]: 1153 info = [ 1154 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1155 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1156 self._ticker, 1157 self._figi, 1158 self.depth, 1159 ), 1160 "-" * 60, "\n", 1161 " Orders of Buyers | Orders of Sellers\n", 1162 "-" * 60, "\n", 1163 " Sell prices (volumes) | Buy prices (volumes)\n", 1164 "-" * 60, "\n", 1165 ] 1166 1167 if not prices["buy"]: 1168 info.append(" | No orders!\n") 1169 sumBuy = 0 1170 1171 else: 1172 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1173 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1174 for item in maxMinSorted: 1175 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1176 1177 if not prices["sell"]: 1178 info.append("No orders! |\n") 1179 sumSell = 0 1180 1181 else: 1182 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1183 for item in prices["sell"]: 1184 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1185 1186 info.extend([ 1187 "-" * 60, "\n", 1188 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1189 "-" * 60, "\n", 1190 ]) 1191 1192 infoText = "".join(info) 1193 1194 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1195 1196 else: 1197 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1198 1199 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1201 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1202 """ 1203 This method get and show information about all available broker instruments for current user account. 1204 If `instrumentsFile` string is not empty then also save information to this file. 1205 1206 :param show: if `True` then print results to console, if `False` — print only to file. 1207 :return: multi-lines string with all available broker instruments 1208 """ 1209 if not self.iList: 1210 self.iList = self.Listing() 1211 1212 info = [ 1213 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1214 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1215 ] 1216 1217 # add instruments count by type: 1218 for iType in self.iList.keys(): 1219 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1220 1221 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1222 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1223 1224 # generating info tables with all instruments by type: 1225 for iType in self.iList.keys(): 1226 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1227 1228 for instrument in self.iList[iType].keys(): 1229 iName = self.iList[iType][instrument]["name"] # instrument's name 1230 if len(iName) > 57: 1231 iName = "{}...".format(iName[:54]) # right trim for a long string 1232 1233 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1234 self.iList[iType][instrument]["ticker"], 1235 iName, 1236 self.iList[iType][instrument]["figi"], 1237 self.iList[iType][instrument]["currency"], 1238 self.iList[iType][instrument]["lot"], 1239 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1240 )) 1241 1242 infoText = "".join(info) 1243 1244 if show: 1245 uLogger.info(infoText) 1246 1247 if self.instrumentsFile: 1248 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1249 fH.write(infoText) 1250 1251 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1252 1253 if self.useHTMLReports: 1254 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1255 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1256 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1257 1258 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1259 1260 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file.
Returns
multi-lines string with all available broker instruments
1262 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1263 """ 1264 This method search and show information about instruments by part of its ticker, FIGI or name. 1265 If `searchResultsFile` string is not empty then also save information to this file. 1266 1267 :param pattern: string with part of ticker, FIGI or instrument's name. 1268 :param show: if `True` then print results to console, if `False` — return list of result only. 1269 :return: list of dictionaries with all found instruments. 1270 """ 1271 if not self.iList: 1272 self.iList = self.Listing() 1273 1274 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1275 compiledPattern = re.compile(pattern, re.IGNORECASE) 1276 1277 for iType in self.iList: 1278 for instrument in self.iList[iType].values(): 1279 searchResult = compiledPattern.search(" ".join( 1280 [instrument["ticker"], instrument["figi"], instrument["name"]] 1281 )) 1282 1283 if searchResult: 1284 searchResults[iType][instrument["ticker"]] = instrument 1285 1286 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1287 info = [ 1288 "# Search results\n\n", 1289 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1290 "* **Search pattern:** [{}]\n".format(pattern), 1291 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1292 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1293 ] 1294 infoShort = info[:] 1295 1296 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1297 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1298 skippedLine = "| ... | ... | ... | ... |\n" 1299 1300 if resultsLen == 0: 1301 info.append("\nNo results\n") 1302 infoShort.append("\nNo results\n") 1303 uLogger.warning("No results. Try changing your search pattern.") 1304 1305 else: 1306 for iType in searchResults: 1307 iTypeValuesCount = len(searchResults[iType].values()) 1308 if iTypeValuesCount > 0: 1309 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1310 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1311 1312 for instrument in searchResults[iType].values(): 1313 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1314 instrument["type"], 1315 instrument["ticker"], 1316 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1317 instrument["figi"], 1318 )) 1319 1320 if iTypeValuesCount <= 5: 1321 infoShort.extend(info[-iTypeValuesCount:]) 1322 1323 else: 1324 infoShort.extend(info[-5:]) 1325 infoShort.append(skippedLine) 1326 1327 infoText = "".join(info) 1328 infoTextShort = "".join(infoShort) 1329 1330 if show: 1331 uLogger.info(infoTextShort) 1332 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1333 1334 if self.searchResultsFile: 1335 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1336 fH.write(infoText) 1337 1338 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1339 1340 if self.useHTMLReports: 1341 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1342 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1343 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1344 1345 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1346 1347 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only.
Returns
list of dictionaries with all found instruments.
1349 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1350 """ 1351 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1352 1353 :param instruments: list of strings with tickers or FIGIs. 1354 :return: list with unique instrument FIGIs only. 1355 """ 1356 requestedInstruments = [] 1357 for iName in instruments: 1358 if iName not in self.aliases.keys(): 1359 if iName not in requestedInstruments: 1360 requestedInstruments.append(iName) 1361 1362 else: 1363 if iName not in requestedInstruments: 1364 if self.aliases[iName] not in requestedInstruments: 1365 requestedInstruments.append(self.aliases[iName]) 1366 1367 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1368 1369 onlyUniqueFIGIs = [] 1370 for iName in requestedInstruments: 1371 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1372 continue 1373 1374 self._ticker = iName 1375 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1376 1377 if not iData: 1378 self._ticker = "" 1379 self._figi = iName 1380 1381 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1382 1383 if not iData: 1384 self._figi = "" 1385 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1386 1387 if iData and iData["figi"] not in onlyUniqueFIGIs: 1388 onlyUniqueFIGIs.append(iData["figi"]) 1389 1390 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1391 1392 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1394 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1395 """ 1396 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1397 1398 See limits: https://tinkoff.github.io/investAPI/limits/ 1399 1400 If `pricesFile` string is not empty then also save information to this file. 1401 1402 :param instruments: list of strings with tickers or FIGIs. 1403 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1404 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1405 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1406 """ 1407 if instruments is None or not instruments: 1408 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1409 raise Exception("Ticker or FIGI required") 1410 1411 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1412 1413 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1414 1415 iList = [] # trying to get info and current prices about all unique instruments: 1416 for self._figi in onlyUniqueFIGIs: 1417 iData = self.SearchByFIGI(requestPrice=True) 1418 iList.append(iData) 1419 1420 self.ShowListOfPrices(iList, show) 1421 1422 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1424 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1425 """ 1426 Show table contains current prices of given instruments. 1427 1428 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1429 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1430 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1431 :return: multilines text in Markdown format as a table contains current prices. 1432 """ 1433 infoText = "" 1434 1435 if show or self.pricesFile: 1436 info = [ 1437 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1438 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1439 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1440 ] 1441 1442 for item in iList: 1443 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1444 item["ticker"], 1445 item["figi"], 1446 item["type"], 1447 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1448 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1449 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1450 "{} / {}".format( 1451 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1452 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1453 ), 1454 "{} / {}".format( 1455 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1456 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1457 ), 1458 item["currency"], 1459 )) 1460 1461 infoText = "".join(info) 1462 1463 if show: 1464 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1465 1466 if self.pricesFile: 1467 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1468 fH.write(infoText) 1469 1470 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1471 1472 if self.useHTMLReports: 1473 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1474 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1475 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1476 1477 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1478 1479 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1481 def RequestTradingStatus(self) -> dict: 1482 """ 1483 Requesting trading status for the instrument defined by `figi` variable. 1484 1485 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1486 1487 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1488 1489 :return: dictionary with trading status attributes. Response example: 1490 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1491 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1492 """ 1493 if self._figi is None or not self._figi: 1494 uLogger.error("Variable `figi` must be defined for using this method!") 1495 raise Exception("FIGI required") 1496 1497 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1498 1499 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1500 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1501 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1502 1503 if self.moreDebug: 1504 uLogger.debug("Records about current trading status successfully received") 1505 1506 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1508 def RequestPortfolio(self) -> dict: 1509 """ 1510 Requesting actual user's portfolio for current `accountId`. 1511 1512 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1513 1514 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1515 1516 :return: dictionary with user's portfolio. 1517 """ 1518 if self.accountId is None or not self.accountId: 1519 uLogger.error("Variable `accountId` must be defined for using this method!") 1520 raise Exception("Account ID required") 1521 1522 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1523 1524 self.body = str({"accountId": self.accountId}) 1525 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1526 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1527 1528 if self.moreDebug: 1529 uLogger.debug("Records about user's portfolio successfully received") 1530 1531 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1533 def RequestPositions(self) -> dict: 1534 """ 1535 Requesting open positions by currencies and instruments for current `accountId`. 1536 1537 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1538 1539 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1540 1541 :return: dictionary with open positions by instruments. 1542 """ 1543 if self.accountId is None or not self.accountId: 1544 uLogger.error("Variable `accountId` must be defined for using this method!") 1545 raise Exception("Account ID required") 1546 1547 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1548 1549 self.body = str({"accountId": self.accountId}) 1550 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1551 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1552 1553 if self.moreDebug: 1554 uLogger.debug("Records about current open positions successfully received") 1555 1556 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1558 def RequestPendingOrders(self) -> list: 1559 """ 1560 Requesting current actual pending limit orders for current `accountId`. 1561 1562 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1563 1564 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1565 1566 :return: list of dictionaries with pending limit orders. 1567 """ 1568 if self.accountId is None or not self.accountId: 1569 uLogger.error("Variable `accountId` must be defined for using this method!") 1570 raise Exception("Account ID required") 1571 1572 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1573 1574 self.body = str({"accountId": self.accountId}) 1575 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1576 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1577 1578 if "orders" in rawResponse.keys(): 1579 rawOrders = rawResponse["orders"] 1580 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1581 1582 else: 1583 rawOrders = [] 1584 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1585 1586 return rawOrders
Requesting current actual pending limit orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending limit orders.
1588 def RequestStopOrders(self) -> list: 1589 """ 1590 Requesting current actual stop orders for current `accountId`. 1591 1592 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1593 1594 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1595 1596 :return: list of dictionaries with stop orders. 1597 """ 1598 if self.accountId is None or not self.accountId: 1599 uLogger.error("Variable `accountId` must be defined for using this method!") 1600 raise Exception("Account ID required") 1601 1602 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1603 1604 self.body = str({"accountId": self.accountId}) 1605 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1606 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1607 1608 if "stopOrders" in rawResponse.keys(): 1609 rawStopOrders = rawResponse["stopOrders"] 1610 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1611 1612 else: 1613 rawStopOrders = [] 1614 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1615 1616 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1618 def Overview(self, show: bool = False, details: str = "full") -> dict: 1619 """ 1620 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1621 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1622 and `overviewBondsCalendarFile` are defined then also save information to file. 1623 1624 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1625 many requests about the state of the portfolio, and then, based on the received data, a large number 1626 of calculation and statistics are collected. 1627 1628 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1629 :param details: how detailed should the information be? 1630 - `full` — shows full available information about portfolio status (by default), 1631 - `positions` — shows only open positions, 1632 - `orders` — shows only sections of open limits and stop orders. 1633 - `digest` — show a short digest of the portfolio status, 1634 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1635 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1636 :return: dictionary with client's raw portfolio and some statistics. 1637 """ 1638 if self.accountId is None or not self.accountId: 1639 uLogger.error("Variable `accountId` must be defined for using this method!") 1640 raise Exception("Account ID required") 1641 1642 view = { 1643 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1644 "headers": {}, # list of dictionaries, response headers without "positions" section 1645 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1646 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1647 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1648 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1649 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1650 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1651 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1652 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1653 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1654 }, 1655 "stat": { # --- some statistics calculated using "raw" sections: 1656 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1657 "availableRUB": 0., # available rubles (without other currencies) 1658 "blockedRUB": 0., # blocked sum in Russian Rouble 1659 "totalChangesRUB": 0., # changes for all open trades in RUB 1660 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1661 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1662 "sharesCostRUB": 0., # costs of all shares in RUB 1663 "bondsCostRUB": 0., # costs of all bonds in RUB 1664 "etfsCostRUB": 0., # costs of all etfs in RUB 1665 "futuresCostRUB": 0., # costs of all futures in RUB 1666 "Currencies": [], # list of dictionaries of all currencies statistics 1667 "Shares": [], # list of dictionaries of all shares statistics 1668 "Bonds": [], # list of dictionaries of all bonds statistics 1669 "Etfs": [], # list of dictionaries of all etfs statistics 1670 "Futures": [], # list of dictionaries of all futures statistics 1671 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1672 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1673 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1674 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1675 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1676 }, 1677 "analytics": { # --- some analytics of portfolio: 1678 "distrByAssets": {}, # portfolio distribution by assets 1679 "distrByCompanies": {}, # portfolio distribution by companies 1680 "distrBySectors": {}, # portfolio distribution by sectors 1681 "distrByCurrencies": {}, # portfolio distribution by currencies 1682 "distrByCountries": {}, # portfolio distribution by countries 1683 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1684 } 1685 } 1686 1687 details = details.lower() 1688 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1689 if details not in availableDetails: 1690 details = "full" 1691 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1692 1693 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1694 1695 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1696 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1697 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1698 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1699 1700 # save response headers without "positions" section: 1701 for key in portfolioResponse.keys(): 1702 if key != "positions": 1703 view["raw"]["headers"][key] = portfolioResponse[key] 1704 1705 else: 1706 continue 1707 1708 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1709 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1710 for item in portfolioResponse["positions"]: 1711 if item["instrumentType"] == "currency": 1712 self._figi = item["figi"] 1713 if not self._figi and item["ticker"]: 1714 self._ticker = item["ticker"] 1715 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1716 1717 curr = self.SearchByFIGI(requestPrice=False) 1718 1719 # current price of currency in RUB: 1720 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1721 "name": curr["name"], 1722 "currentPrice": NanoToFloat( 1723 item["currentPrice"]["units"], 1724 item["currentPrice"]["nano"] 1725 ), 1726 } 1727 1728 view["raw"]["Currencies"].append(item) 1729 1730 elif item["instrumentType"] == "share": 1731 view["raw"]["Shares"].append(item) 1732 1733 elif item["instrumentType"] == "bond": 1734 view["raw"]["Bonds"].append(item) 1735 1736 elif item["instrumentType"] == "etf": 1737 view["raw"]["Etfs"].append(item) 1738 1739 elif item["instrumentType"] == "futures": 1740 view["raw"]["Futures"].append(item) 1741 1742 else: 1743 continue 1744 1745 # how many volume of currencies (by ISO currency name) are blocked: 1746 for item in view["raw"]["positions"]["blocked"]: 1747 blocked = NanoToFloat(item["units"], item["nano"]) 1748 if blocked > 0: 1749 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1750 1751 # how many volume of instruments (by FIGI) are blocked: 1752 for item in view["raw"]["positions"]["securities"]: 1753 blocked = int(item["blocked"]) 1754 if blocked > 0: 1755 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1756 1757 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1758 1759 if "rub" in allBlocked.keys(): 1760 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1761 1762 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1763 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1764 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1765 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1766 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1767 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1768 view["stat"]["portfolioCostRUB"] = sum([ 1769 view["stat"]["allCurrenciesCostRUB"], 1770 view["stat"]["sharesCostRUB"], 1771 view["stat"]["bondsCostRUB"], 1772 view["stat"]["etfsCostRUB"], 1773 view["stat"]["futuresCostRUB"], 1774 ]) 1775 1776 # --- calculating some portfolio statistics: 1777 byComp = {} # distribution by companies 1778 bySect = {} # distribution by sectors 1779 byCurr = {} # distribution by currencies (include RUB) 1780 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1781 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1782 1783 for item in portfolioResponse["positions"]: 1784 self._figi = item["figi"] 1785 if not self._figi and item["ticker"]: 1786 self._ticker = item["ticker"] 1787 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1788 1789 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1790 1791 if instrument: 1792 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1793 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1794 1795 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1796 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1797 1798 else: 1799 blocked = 0 1800 1801 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1802 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1803 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1804 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1805 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1806 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1807 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1808 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1809 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1810 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1811 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1812 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1813 1814 statData = { 1815 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1816 "ticker": instrument["ticker"], # ticker by FIGI 1817 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1818 "volume": volume, # available volume of instrument 1819 "lots": lots, # volume in lots of instrument 1820 "direction": direction, # direction of an instrument's position: short or long 1821 "blocked": blocked, # blocked volume of currency or instrument 1822 "currentPrice": curPrice, # current instrument's price in basic asset 1823 "average": average, # current average position price 1824 "cost": cost, # current cost of all volume of instrument in basic asset 1825 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1826 "costRUB": costRUB, # cost of instrument in ruble 1827 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1828 "profit": profit, # expected profit at current moment 1829 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1830 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1831 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1832 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1833 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1834 "step": instrument["step"], # minimum price increment 1835 } 1836 1837 # adding distribution by unique countries: 1838 if statData["country"] not in byCountry.keys(): 1839 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1840 1841 else: 1842 byCountry[statData["country"]]["cost"] += costRUB 1843 byCountry[statData["country"]]["percent"] += percentCostRUB 1844 1845 if item["instrumentType"] != "currency": 1846 # adding distribution by unique companies: 1847 if statData["name"]: 1848 if statData["name"] not in byComp.keys(): 1849 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1850 1851 else: 1852 byComp[statData["name"]]["cost"] += costRUB 1853 byComp[statData["name"]]["percent"] += percentCostRUB 1854 1855 # adding distribution by unique sectors: 1856 if statData["sector"] not in bySect.keys(): 1857 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1858 1859 else: 1860 bySect[statData["sector"]]["cost"] += costRUB 1861 bySect[statData["sector"]]["percent"] += percentCostRUB 1862 1863 # adding distribution by unique currencies: 1864 if currency not in byCurr.keys(): 1865 byCurr[currency] = { 1866 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1867 "cost": costRUB, 1868 "percent": percentCostRUB 1869 } 1870 1871 else: 1872 byCurr[currency]["cost"] += costRUB 1873 byCurr[currency]["percent"] += percentCostRUB 1874 1875 # saving statistics for every instrument: 1876 if item["instrumentType"] == "currency": 1877 view["stat"]["Currencies"].append(statData) 1878 1879 # update dict with free funds for trading (total - blocked) by currencies 1880 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1881 view["stat"]["funds"][currency] = { 1882 "total": volume, 1883 "totalCostRUB": costRUB, # total volume cost in rubles 1884 "free": volume - blocked, 1885 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1886 } 1887 1888 elif item["instrumentType"] == "share": 1889 view["stat"]["Shares"].append(statData) 1890 1891 elif item["instrumentType"] == "bond": 1892 view["stat"]["Bonds"].append(statData) 1893 1894 elif item["instrumentType"] == "etf": 1895 view["stat"]["Etfs"].append(statData) 1896 1897 elif item["instrumentType"] == "Futures": 1898 view["stat"]["Futures"].append(statData) 1899 1900 else: 1901 continue 1902 1903 # total changes in Russian Ruble: 1904 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1905 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1906 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1907 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1908 view["stat"]["funds"]["rub"] = { 1909 "total": view["stat"]["availableRUB"], 1910 "totalCostRUB": view["stat"]["availableRUB"], 1911 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1912 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1913 } 1914 1915 # --- pending limit orders sector data: 1916 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1917 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1918 1919 for item in view["raw"]["orders"]: 1920 self._figi = item["figi"] 1921 1922 if item["figi"] not in uniquePendingOrdersFIGIs: 1923 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1924 1925 uniquePendingOrdersFIGIs.append(item["figi"]) 1926 uniquePendingOrders[item["figi"]] = instrument 1927 1928 else: 1929 instrument = uniquePendingOrders[item["figi"]] 1930 1931 if instrument: 1932 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1933 orderType = TKS_ORDER_TYPES[item["orderType"]] 1934 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1935 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1936 1937 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1938 if item["direction"] == "ORDER_DIRECTION_BUY": 1939 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1940 1941 else: 1942 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1943 1944 # requested price for order execution: 1945 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1946 1947 # necessary changes in percent to reach target from current price: 1948 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1949 1950 view["stat"]["orders"].append({ 1951 "orderID": item["orderId"], # orderId number parameter of current order 1952 "figi": item["figi"], # FIGI identification 1953 "ticker": instrument["ticker"], # ticker name by FIGI 1954 "lotsRequested": item["lotsRequested"], # requested lots value 1955 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1956 "currentPrice": lastPrice, # current instrument's price for defined action 1957 "targetPrice": target, # requested price for order execution in base currency 1958 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1959 "percentChanges": changes, # changes in percent to target from current price 1960 "currency": item["currency"], # instrument's currency name 1961 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1962 "type": orderType, # type of order from TKS_ORDER_TYPES 1963 "status": orderState, # order status from TKS_ORDER_STATES 1964 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1965 }) 1966 1967 # --- stop orders sector data: 1968 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1969 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1970 1971 for item in view["raw"]["stopOrders"]: 1972 self._figi = item["figi"] 1973 1974 if item["figi"] not in uniqueStopOrdersFIGIs: 1975 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1976 1977 uniqueStopOrdersFIGIs.append(item["figi"]) 1978 uniqueStopOrders[item["figi"]] = instrument 1979 1980 else: 1981 instrument = uniqueStopOrders[item["figi"]] 1982 1983 if instrument: 1984 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1985 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1986 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1987 1988 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1989 if "expirationTime" in item.keys(): 1990 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1991 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1992 1993 else: 1994 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1995 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1996 1997 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1998 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1999 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2000 2001 else: 2002 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2003 2004 # requested price when stop-order executed: 2005 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2006 2007 # price for limit-order, set up when stop-order executed: 2008 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2009 2010 # necessary changes in percent to reach target from current price: 2011 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2012 2013 view["stat"]["stopOrders"].append({ 2014 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2015 "figi": item["figi"], # FIGI identification 2016 "ticker": instrument["ticker"], # ticker name by FIGI 2017 "lotsRequested": item["lotsRequested"], # requested lots value 2018 "currentPrice": lastPrice, # current instrument's price for defined action 2019 "targetPrice": target, # requested price for stop-order execution in base currency 2020 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2021 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2022 "percentChanges": changes, # changes in percent to target from current price 2023 "currency": item["currency"], # instrument's currency name 2024 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2025 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2026 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2027 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2028 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2029 }) 2030 2031 # --- calculating data for analytics section: 2032 # portfolio distribution by assets: 2033 view["analytics"]["distrByAssets"] = { 2034 "Ruble": { 2035 "uniques": 1, 2036 "cost": view["stat"]["availableRUB"], 2037 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2038 }, 2039 "Currencies": { 2040 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2041 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2042 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2043 }, 2044 "Shares": { 2045 "uniques": len(view["stat"]["Shares"]), 2046 "cost": view["stat"]["sharesCostRUB"], 2047 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2048 }, 2049 "Bonds": { 2050 "uniques": len(view["stat"]["Bonds"]), 2051 "cost": view["stat"]["bondsCostRUB"], 2052 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2053 }, 2054 "Etfs": { 2055 "uniques": len(view["stat"]["Etfs"]), 2056 "cost": view["stat"]["etfsCostRUB"], 2057 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2058 }, 2059 "Futures": { 2060 "uniques": len(view["stat"]["Futures"]), 2061 "cost": view["stat"]["futuresCostRUB"], 2062 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2063 }, 2064 } 2065 2066 # portfolio distribution by companies: 2067 view["analytics"]["distrByCompanies"]["All money cash"] = { 2068 "ticker": "", 2069 "cost": view["stat"]["allCurrenciesCostRUB"], 2070 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2071 } 2072 view["analytics"]["distrByCompanies"].update(byComp) 2073 2074 # portfolio distribution by sectors: 2075 view["analytics"]["distrBySectors"]["All money cash"] = { 2076 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2077 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2078 } 2079 view["analytics"]["distrBySectors"].update(bySect) 2080 2081 # portfolio distribution by currencies: 2082 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2083 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2084 2085 if self.moreDebug: 2086 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2087 2088 view["analytics"]["distrByCurrencies"].update(byCurr) 2089 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2090 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2091 2092 # portfolio distribution by countries: 2093 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2094 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2095 2096 if self.moreDebug: 2097 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2098 2099 view["analytics"]["distrByCountries"].update(byCountry) 2100 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2101 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2102 2103 # --- Prepare text statistics overview in human-readable: 2104 if show: 2105 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2106 2107 # Whatever the value `details`, header not changes: 2108 info = [ 2109 "# Client's portfolio\n\n", 2110 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2111 "* **Account ID:** [{}]\n".format(self.accountId), 2112 ] 2113 2114 if details in ["full", "positions", "digest"]: 2115 info.extend([ 2116 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2117 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2118 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2119 view["stat"]["totalChangesRUB"], 2120 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2121 view["stat"]["totalChangesPercentRUB"], 2122 ), 2123 ]) 2124 2125 if details in ["full", "positions"]: 2126 info.extend([ 2127 "## Open positions\n\n", 2128 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2129 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2130 "| **Ruble:** | {:>31} | | | | | |\n".format( 2131 "{:.2f} ({:.2f}) rub".format( 2132 view["stat"]["availableRUB"], 2133 view["stat"]["blockedRUB"], 2134 ) 2135 ) 2136 ]) 2137 2138 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2139 return [ 2140 "| | | | | | | |\n", 2141 "| {:<27} | | | | | {:>19} | |\n".format( 2142 noTradeStr if noTradeStr else typeStr, 2143 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2144 ), 2145 ] 2146 2147 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2148 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2149 "{} [{}]".format(data["ticker"], data["figi"]), 2150 "{:.2f} ({:.2f}) {}".format( 2151 data["volume"], 2152 data["blocked"], 2153 data["currency"], 2154 ) if isCurr else "{:.0f} ({:.0f})".format( 2155 data["volume"], 2156 data["blocked"], 2157 ), 2158 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2159 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2160 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2161 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2162 "{}{:.2f} {} ({}{:.2f}%)".format( 2163 "+" if data["profit"] > 0 else "", 2164 data["profit"], data["baseCurrencyName"], 2165 "+" if data["percentProfit"] > 0 else "", 2166 data["percentProfit"], 2167 ), 2168 ) 2169 2170 # --- Show currencies section: 2171 if view["stat"]["Currencies"]: 2172 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2173 for item in view["stat"]["Currencies"]: 2174 info.append(_InfoStr(item, isCurr=True)) 2175 2176 else: 2177 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2178 2179 # --- Show shares section: 2180 if view["stat"]["Shares"]: 2181 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2182 2183 for item in view["stat"]["Shares"]: 2184 info.append(_InfoStr(item)) 2185 2186 else: 2187 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2188 2189 # --- Show bonds section: 2190 if view["stat"]["Bonds"]: 2191 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2192 2193 for item in view["stat"]["Bonds"]: 2194 info.append(_InfoStr(item)) 2195 2196 else: 2197 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2198 2199 # --- Show etfs section: 2200 if view["stat"]["Etfs"]: 2201 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2202 2203 for item in view["stat"]["Etfs"]: 2204 info.append(_InfoStr(item)) 2205 2206 else: 2207 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2208 2209 # --- Show futures section: 2210 if view["stat"]["Futures"]: 2211 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2212 2213 for item in view["stat"]["Futures"]: 2214 info.append(_InfoStr(item)) 2215 2216 else: 2217 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2218 2219 if details in ["full", "orders"]: 2220 # --- Show pending limit orders section: 2221 if view["stat"]["orders"]: 2222 info.extend([ 2223 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2224 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2225 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2226 ]) 2227 2228 for item in view["stat"]["orders"]: 2229 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2230 "{} [{}]".format(item["ticker"], item["figi"]), 2231 item["orderID"], 2232 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2233 "{} {} ({}{:.2f}%)".format( 2234 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2235 item["baseCurrencyName"], 2236 "+" if item["percentChanges"] > 0 else "", 2237 float(item["percentChanges"]), 2238 ), 2239 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2240 item["action"], 2241 item["type"], 2242 item["date"], 2243 )) 2244 2245 else: 2246 info.append("\n## Total pending limit-orders: [0]\n") 2247 2248 # --- Show stop orders section: 2249 if view["stat"]["stopOrders"]: 2250 info.extend([ 2251 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2252 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2253 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2254 ]) 2255 2256 for item in view["stat"]["stopOrders"]: 2257 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2258 "{} [{}]".format(item["ticker"], item["figi"]), 2259 item["orderID"], 2260 item["lotsRequested"], 2261 "{} {} ({}{:.2f}%)".format( 2262 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2263 item["baseCurrencyName"], 2264 "+" if item["percentChanges"] > 0 else "", 2265 float(item["percentChanges"]), 2266 ), 2267 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2268 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2269 item["action"], 2270 item["type"], 2271 item["expType"], 2272 item["createDate"], 2273 item["expDate"], 2274 )) 2275 2276 else: 2277 info.append("\n## Total stop-orders: [0]\n") 2278 2279 if details in ["full", "analytics"]: 2280 # -- Show analytics section: 2281 if view["stat"]["portfolioCostRUB"] > 0: 2282 info.extend([ 2283 "\n# Analytics\n\n" 2284 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2285 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2286 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2287 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2288 view["stat"]["totalChangesRUB"], 2289 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2290 view["stat"]["totalChangesPercentRUB"], 2291 ), 2292 "\n## Portfolio distribution by assets\n" 2293 "\n| Type | Uniques | Percent | Current cost |\n", 2294 "|------------------------------------|---------|---------|--------------------|\n", 2295 ]) 2296 2297 for key in view["analytics"]["distrByAssets"].keys(): 2298 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2299 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2300 key, 2301 view["analytics"]["distrByAssets"][key]["uniques"], 2302 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2303 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2304 )) 2305 2306 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2307 2308 info.extend([ 2309 "\n## Portfolio distribution by companies\n" 2310 "\n| Company | Percent | Current cost |\n", 2311 aSepLine, 2312 ]) 2313 2314 for company in view["analytics"]["distrByCompanies"].keys(): 2315 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2316 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2317 "{}{}".format( 2318 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2319 company, 2320 ), 2321 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2322 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2323 )) 2324 2325 info.extend([ 2326 "\n## Portfolio distribution by sectors\n" 2327 "\n| Sector | Percent | Current cost |\n", 2328 aSepLine, 2329 ]) 2330 2331 for sector in view["analytics"]["distrBySectors"].keys(): 2332 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2333 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2334 sector, 2335 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2336 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2337 )) 2338 2339 info.extend([ 2340 "\n## Portfolio distribution by currencies\n" 2341 "\n| Instruments currencies | Percent | Current cost |\n", 2342 aSepLine, 2343 ]) 2344 2345 for curr in view["analytics"]["distrByCurrencies"].keys(): 2346 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2347 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2348 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2349 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2350 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2351 )) 2352 2353 info.extend([ 2354 "\n## Portfolio distribution by countries\n" 2355 "\n| Assets by country | Percent | Current cost |\n", 2356 aSepLine, 2357 ]) 2358 2359 for country in view["analytics"]["distrByCountries"].keys(): 2360 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2361 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2362 country, 2363 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2364 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2365 )) 2366 2367 if details in ["full", "calendar"]: 2368 # -- Show bonds payment calendar section: 2369 if view["stat"]["Bonds"]: 2370 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2371 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2372 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2373 2374 else: 2375 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2376 2377 infoText = "".join(info) 2378 2379 uLogger.info(infoText) 2380 2381 if details == "full" and self.overviewFile: 2382 filename = self.overviewFile 2383 2384 elif details == "digest" and self.overviewDigestFile: 2385 filename = self.overviewDigestFile 2386 2387 elif details == "positions" and self.overviewPositionsFile: 2388 filename = self.overviewPositionsFile 2389 2390 elif details == "orders" and self.overviewOrdersFile: 2391 filename = self.overviewOrdersFile 2392 2393 elif details == "analytics" and self.overviewAnalyticsFile: 2394 filename = self.overviewAnalyticsFile 2395 2396 elif details == "calendar" and self.overviewBondsCalendarFile: 2397 filename = self.overviewBondsCalendarFile 2398 2399 else: 2400 filename = "" 2401 2402 if filename: 2403 with open(filename, "w", encoding="UTF-8") as fH: 2404 fH.write(infoText) 2405 2406 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2407 2408 if self.useHTMLReports: 2409 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2410 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2411 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2412 2413 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2414 2415 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio),
Returns
dictionary with client's raw portfolio and some statistics.
2417 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2418 """ 2419 Returns history operations between two given dates for current `accountId`. 2420 If `reportFile` string is not empty then also save human-readable report. 2421 Shows some statistical data of closed positions. 2422 2423 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2424 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2425 :param show: if `True` then also prints all records to the console. 2426 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2427 :return: original list of dictionaries with history of deals records from API ("operations" key): 2428 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2429 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2430 """ 2431 if self.accountId is None or not self.accountId: 2432 uLogger.error("Variable `accountId` must be defined for using this method!") 2433 raise Exception("Account ID required") 2434 2435 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2436 2437 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2438 2439 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2440 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2441 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2442 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2443 customStat = {} # custom statistics in additional to responseJSON 2444 2445 # --- output report in human-readable format: 2446 if show or self.reportFile: 2447 splitLine1 = "| | | | | |\n" # Summary section 2448 splitLine2 = "| | | | | | | | |\n" # Operations section 2449 nextDay = "" 2450 2451 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2452 2453 if len(ops) > 0: 2454 customStat = { 2455 "opsCount": 0, # total operations count 2456 "buyCount": 0, # buy operations 2457 "sellCount": 0, # sell operations 2458 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2459 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2460 "payIn": {"rub": 0.}, # Deposit brokerage account 2461 "payOut": {"rub": 0.}, # Withdrawals 2462 "divs": {"rub": 0.}, # Dividends income 2463 "coupons": {"rub": 0.}, # Coupon's income 2464 "brokerCom": {"rub": 0.}, # Service commissions 2465 "serviceCom": {"rub": 0.}, # Service commissions 2466 "marginCom": {"rub": 0.}, # Margin commissions 2467 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2468 } 2469 2470 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2471 for item in ops: 2472 if item["state"] == "OPERATION_STATE_EXECUTED": 2473 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2474 2475 # count buy operations: 2476 if "_BUY" in item["operationType"]: 2477 customStat["buyCount"] += 1 2478 2479 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2480 customStat["buyTotal"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["buyTotal"][item["payment"]["currency"]] = payment 2484 2485 # count sell operations: 2486 elif "_SELL" in item["operationType"]: 2487 customStat["sellCount"] += 1 2488 2489 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2490 customStat["sellTotal"][item["payment"]["currency"]] += payment 2491 2492 else: 2493 customStat["sellTotal"][item["payment"]["currency"]] = payment 2494 2495 # count incoming operations: 2496 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2497 if item["payment"]["currency"] in customStat["payIn"].keys(): 2498 customStat["payIn"][item["payment"]["currency"]] += payment 2499 2500 else: 2501 customStat["payIn"][item["payment"]["currency"]] = payment 2502 2503 # count withdrawals operations: 2504 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2505 if item["payment"]["currency"] in customStat["payOut"].keys(): 2506 customStat["payOut"][item["payment"]["currency"]] += payment 2507 2508 else: 2509 customStat["payOut"][item["payment"]["currency"]] = payment 2510 2511 # count dividends income: 2512 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2513 if item["payment"]["currency"] in customStat["divs"].keys(): 2514 customStat["divs"][item["payment"]["currency"]] += payment 2515 2516 else: 2517 customStat["divs"][item["payment"]["currency"]] = payment 2518 2519 # count coupon's income: 2520 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2521 if item["payment"]["currency"] in customStat["coupons"].keys(): 2522 customStat["coupons"][item["payment"]["currency"]] += payment 2523 2524 else: 2525 customStat["coupons"][item["payment"]["currency"]] = payment 2526 2527 # count broker commissions: 2528 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2529 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2530 customStat["brokerCom"][item["payment"]["currency"]] += payment 2531 2532 else: 2533 customStat["brokerCom"][item["payment"]["currency"]] = payment 2534 2535 # count service commissions: 2536 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2537 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2538 customStat["serviceCom"][item["payment"]["currency"]] += payment 2539 2540 else: 2541 customStat["serviceCom"][item["payment"]["currency"]] = payment 2542 2543 # count margin commissions: 2544 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2545 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2546 customStat["marginCom"][item["payment"]["currency"]] += payment 2547 2548 else: 2549 customStat["marginCom"][item["payment"]["currency"]] = payment 2550 2551 # count withholding taxes: 2552 elif "_TAX" in item["operationType"]: 2553 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2554 customStat["allTaxes"][item["payment"]["currency"]] += payment 2555 2556 else: 2557 customStat["allTaxes"][item["payment"]["currency"]] = payment 2558 2559 else: 2560 continue 2561 2562 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2563 2564 # --- view "Actions" lines: 2565 info.extend([ 2566 "| Report sections | | | | |\n", 2567 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2568 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2569 "| | Buy: {:<22} | {:<28} | | |\n".format( 2570 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2571 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2572 ), 2573 "| | Sell: {:<21} | {:<28} | | |\n".format( 2574 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2575 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2576 ), 2577 ]) 2578 2579 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2580 for key in opsKeys: 2581 if key == "rub": 2582 continue 2583 2584 info.extend([ 2585 "| | | {:<28} | | |\n".format( 2586 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2587 ), 2588 "| | | {:<28} | | |\n".format( 2589 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2590 ), 2591 ]) 2592 2593 info.append(splitLine1) 2594 2595 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2596 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2597 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2598 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2599 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2600 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2601 ) 2602 2603 # --- view "Payments" lines: 2604 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2605 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2606 2607 for key in paymentsKeys: 2608 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2609 2610 info.append(splitLine1) 2611 2612 # --- view "Commissions and taxes" lines: 2613 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2614 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2615 2616 for key in comKeys: 2617 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2618 2619 info.extend([ 2620 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2621 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2622 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2623 ]) 2624 2625 else: 2626 info.append("Broker returned no operations during this period\n") 2627 2628 # --- view "Operations" section: 2629 for item in ops: 2630 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2631 continue 2632 2633 else: 2634 self._figi = item["figi"] 2635 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2636 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2637 2638 # group of deals during one day: 2639 if nextDay and item["date"].split("T")[0] != nextDay: 2640 info.append(splitLine2) 2641 nextDay = "" 2642 2643 else: 2644 nextDay = item["date"].split("T")[0] # saving current day for splitting 2645 2646 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2647 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2648 self._figi if self._figi else "—", 2649 instrument["ticker"] if instrument else "—", 2650 instrument["type"] if instrument else "—", 2651 item["quantity"] if int(item["quantity"]) > 0 else "—", 2652 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2653 TKS_OPERATION_STATES[item["state"]], 2654 TKS_OPERATION_TYPES[item["operationType"]], 2655 )) 2656 2657 infoText = "".join(info) 2658 2659 if show: 2660 if self.moreDebug: 2661 uLogger.debug("Records about history of a client's operations successfully received") 2662 2663 uLogger.info(infoText) 2664 2665 if self.reportFile: 2666 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2667 fH.write(infoText) 2668 2669 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2670 2671 if self.useHTMLReports: 2672 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2673 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2674 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2675 2676 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2677 2678 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2680 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2681 """ 2682 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2683 2684 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2685 Warning! Broker server used ISO UTC time by default. 2686 2687 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2688 Also, `historyFile` used to update history with `onlyMissing` parameter. 2689 2690 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2691 2692 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2693 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2694 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2695 `"hour"`, `"day"`. Default: `"hour"`. 2696 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2697 False by default. Warning! History appends only from last candle to current time 2698 with always update last candle! 2699 :param csvSep: separator if csv-file is used, `,` by default. 2700 :param show: if `True` then also prints Pandas DataFrame to the console. 2701 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2702 `["date", "time", "open", "high", "low", "close", "volume"]`. 2703 """ 2704 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2705 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2706 history = None # empty pandas object for history 2707 2708 if interval not in TKS_CANDLE_INTERVALS.keys(): 2709 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2710 raise Exception("Incorrect value") 2711 2712 if not (self._ticker or self._figi): 2713 uLogger.error("Ticker or FIGI must be defined!") 2714 raise Exception("Ticker or FIGI required") 2715 2716 if self._ticker and not self._figi: 2717 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2718 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2719 2720 if self._figi and not self._ticker: 2721 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2722 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2723 2724 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2725 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2726 if interval.lower() != "day": 2727 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2728 2729 delta = dtEnd - dtStart # current UTC time minus last time in file 2730 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2731 2732 # calculate history length in candles: 2733 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2734 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2735 length += 1 # to avoid fraction time 2736 2737 # calculate data blocks count: 2738 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2739 2740 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2741 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2742 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2743 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2744 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2745 2746 tempOld = None # pandas object for old history, if --only-missing key present 2747 lastTime = None # datetime object of last old candle in file 2748 2749 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2750 uLogger.debug("--only-missing key present, add only last missing candles...") 2751 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2752 2753 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2754 2755 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2756 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2757 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2758 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2759 2760 # get last datetime object from last string in file or minus 1 delta if file is empty: 2761 if len(tempOld) > 0: 2762 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2763 2764 else: 2765 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2766 2767 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2768 2769 responseJSONs = [] # raw history blocks of data 2770 2771 blockEnd = dtEnd 2772 for item in range(blocks): 2773 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2774 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2775 2776 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2777 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2778 )) 2779 2780 if blockStart == blockEnd: 2781 uLogger.debug("Skipped this zero-length block...") 2782 2783 else: 2784 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2785 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2786 self.body = str({ 2787 "figi": self._figi, 2788 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2789 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2790 "interval": TKS_CANDLE_INTERVALS[interval][0] 2791 }) 2792 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2793 2794 if "code" in responseJSON.keys(): 2795 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2796 2797 else: 2798 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2799 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2800 2801 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2802 2803 blockEnd = blockStart 2804 2805 printCount = len(responseJSONs) # candles to show in console 2806 if responseJSONs: 2807 tempHistory = pd.DataFrame( 2808 data={ 2809 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2810 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2811 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2812 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2813 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2814 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2815 "volume": [int(item["volume"]) for item in responseJSONs], 2816 }, 2817 index=range(len(responseJSONs)), 2818 columns=["date", "time", "open", "high", "low", "close", "volume"], 2819 ) 2820 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2821 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2822 2823 # append only newest candles to old history if --only-missing key present: 2824 if onlyMissing and tempOld is not None and lastTime is not None: 2825 index = 0 # find start index in tempHistory data: 2826 2827 for i, item in tempHistory.iterrows(): 2828 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2829 2830 if curTime == lastTime: 2831 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2832 index = i 2833 printCount = index + 1 2834 break 2835 2836 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2837 2838 else: 2839 history = tempHistory # if no `--only-missing` key then load full data from server 2840 2841 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2842 2843 if history is not None and not history.empty: 2844 if show: 2845 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2846 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2847 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2848 )) 2849 2850 else: 2851 uLogger.warning("Received an empty candles history!") 2852 2853 if self.historyFile is not None: 2854 if history is not None and not history.empty: 2855 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2856 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2857 2858 else: 2859 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2860 2861 else: 2862 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2863 2864 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2866 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2867 """ 2868 Load candles history from csv-file and return Pandas DataFrame object. 2869 2870 See also: `History()` and `ShowHistoryChart()` methods. 2871 2872 :param filePath: path to csv-file to open. 2873 """ 2874 loadedHistory = None # init candles data object 2875 2876 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2877 2878 if os.path.exists(filePath): 2879 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2880 2881 tfStr = self.priceModel.FormattedDelta( 2882 self.priceModel.timeframe, 2883 "{days} days {hours}h {minutes}m {seconds}s", 2884 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2885 self.priceModel.timeframe, 2886 "{hours}h {minutes}m {seconds}s", 2887 ) 2888 2889 if loadedHistory is not None and not loadedHistory.empty: 2890 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2891 len(loadedHistory), 2892 tfStr, 2893 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2894 ) 2895 2896 else: 2897 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2898 2899 else: 2900 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2901 2902 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2904 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2905 """ 2906 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2907 2908 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2909 Default: `index.html` (both for interact and non-interact candlesticks chart). 2910 2911 See also: `History()` and `LoadHistory()` methods. 2912 2913 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2914 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2915 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2916 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2917 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2918 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2919 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2920 """ 2921 if isinstance(candles, str): 2922 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2923 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2924 2925 elif isinstance(candles, pd.DataFrame): 2926 self.priceModel.prices = candles # set candles chain from variable 2927 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2928 2929 if "datetime" not in candles.columns: 2930 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2931 2932 else: 2933 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2934 raise Exception("Incorrect value") 2935 2936 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2937 2938 if interact: 2939 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2940 2941 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2942 2943 else: 2944 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2945 2946 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2947 2948 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2950 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2951 """ 2952 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2953 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2954 2955 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2956 2957 :param operation: string "Buy" or "Sell". 2958 :param lots: volume, integer count of lots >= 1. 2959 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2960 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2961 :param expDate: string "Undefined" by default or local date in future, 2962 it is a string with format `%Y-%m-%d %H:%M:%S`. 2963 :return: JSON with response from broker server. 2964 """ 2965 if self.accountId is None or not self.accountId: 2966 uLogger.error("Variable `accountId` must be defined for using this method!") 2967 raise Exception("Account ID required") 2968 2969 if operation is None or not operation or operation not in ("Buy", "Sell"): 2970 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2971 raise Exception("Incorrect value") 2972 2973 if lots is None or lots < 1: 2974 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2975 lots = 1 2976 2977 if tp is None or tp < 0: 2978 tp = 0 2979 2980 if sl is None or sl < 0: 2981 sl = 0 2982 2983 if expDate is None or not expDate: 2984 expDate = "Undefined" 2985 2986 if not (self._ticker or self._figi): 2987 uLogger.error("Ticker or FIGI must be defined!") 2988 raise Exception("Ticker or FIGI required") 2989 2990 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2991 self._ticker = instrument["ticker"] 2992 self._figi = instrument["figi"] 2993 2994 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2995 2996 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2997 self.body = str({ 2998 "figi": self._figi, 2999 "quantity": str(lots), 3000 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3001 "accountId": str(self.accountId), 3002 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3003 }) 3004 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3005 3006 if "orderId" in response.keys(): 3007 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3008 operation, response["orderId"], 3009 self._ticker, self._figi, lots, 3010 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3011 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3012 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3013 )) 3014 3015 if tp > 0: 3016 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3017 3018 if sl > 0: 3019 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3020 3021 else: 3022 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3023 3024 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3026 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3027 """ 3028 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3029 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3030 3031 See also: `Order()` and `Trade()` docstrings. 3032 3033 :param lots: volume, integer count of lots >= 1. 3034 :param tp: float > 0, take profit price of stop-order. 3035 :param sl: float > 0, stop loss price of stop-order. 3036 :param expDate: it's a local date in future. 3037 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3038 :return: JSON with response from broker server. 3039 """ 3040 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3042 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3043 """ 3044 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3045 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3046 3047 See also: `Order()` and `Trade()` docstrings. 3048 3049 :param lots: volume, integer count of lots >= 1. 3050 :param tp: float > 0, take profit price of stop-order. 3051 :param sl: float > 0, stop loss price of stop-order. 3052 :param expDate: it's a local date in the future. 3053 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3054 :return: JSON with response from broker server. 3055 """ 3056 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3058 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3059 """ 3060 Close position of given instruments. 3061 3062 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3063 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3064 This avoids unnecessary downloading data from the server. 3065 """ 3066 if instruments is None or not instruments: 3067 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3068 raise Exception("Ticker or FIGI required") 3069 3070 if isinstance(instruments, str): 3071 instruments = [instruments] 3072 3073 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3074 if uniqueInstruments: 3075 if portfolio is None or not portfolio: 3076 portfolio = self.Overview(show=False) 3077 3078 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3079 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3080 3081 for self._figi in uniqueInstruments: 3082 if self._figi not in allOpened: 3083 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3084 continue 3085 3086 # search open trade info about instrument by ticker: 3087 instrument = {} 3088 for iType in TKS_INSTRUMENTS: 3089 if instrument: 3090 break 3091 3092 for item in portfolio["stat"][iType]: 3093 if item["figi"] == self._figi: 3094 instrument = item 3095 break 3096 3097 if instrument: 3098 self._ticker = instrument["ticker"] 3099 self._figi = instrument["figi"] 3100 3101 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3102 self._ticker, 3103 self._figi, 3104 int(instrument["volume"]), 3105 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3106 )) 3107 3108 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3109 3110 if tradeLots > 0: 3111 if instrument["blocked"] > 0: 3112 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3113 instrument["blocked"], 3114 self._ticker, 3115 tradeLots, 3116 )) 3117 3118 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3119 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3120 3121 else: 3122 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3124 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3125 """ 3126 Close all positions of given instruments with defined type. 3127 3128 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3129 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3130 This avoids unnecessary downloading data from the server. 3131 """ 3132 if iType not in TKS_INSTRUMENTS: 3133 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3134 3135 else: 3136 if portfolio is None or not portfolio: 3137 portfolio = self.Overview(show=False) 3138 3139 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3140 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3141 3142 if tickers and portfolio: 3143 self.CloseTrades(tickers, portfolio) 3144 3145 else: 3146 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3148 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3149 """ 3150 Universal method to create market or limit orders with all available parameters for current `accountId`. 3151 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3152 3153 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3154 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3155 3156 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3157 then broker immediately open market order as you can do simple --buy or --sell operations! 3158 3159 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3160 When current price will go up or down to target price value then broker opens a limit order. 3161 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3162 3163 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3164 3165 :param operation: string "Buy" or "Sell". 3166 :param orderType: string "Limit" or "Stop". 3167 :param lots: volume, integer count of lots >= 1. 3168 :param targetPrice: target price > 0. This is open trade price for limit order. 3169 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3170 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3171 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3172 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3173 Stop loss order always executed by market price. 3174 :param expDate: string "Undefined" by default or local date in future. 3175 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3176 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3177 A limit order has no expiration date, it lasts until the end of the trading day. 3178 :return: JSON with response from broker server. 3179 """ 3180 if self.accountId is None or not self.accountId: 3181 uLogger.error("Variable `accountId` must be defined for using this method!") 3182 raise Exception("Account ID required") 3183 3184 if operation is None or not operation or operation not in ("Buy", "Sell"): 3185 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3186 raise Exception("Incorrect value") 3187 3188 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3189 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3190 raise Exception("Incorrect value") 3191 3192 if lots is None or lots < 1: 3193 uLogger.error("You must define trade volume > 0: integer count of lots!") 3194 raise Exception("Incorrect value") 3195 3196 if targetPrice is None or targetPrice <= 0: 3197 uLogger.error("Target price for limit-order must be greater than 0!") 3198 raise Exception("Incorrect value") 3199 3200 if limitPrice is None or limitPrice <= 0: 3201 limitPrice = targetPrice 3202 3203 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3204 stopType = "Limit" 3205 3206 if expDate is None or not expDate: 3207 expDate = "Undefined" 3208 3209 if not (self._ticker or self._figi): 3210 uLogger.error("Tocker or FIGI must be defined!") 3211 raise Exception("Ticker or FIGI required") 3212 3213 response = {} 3214 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3215 self._ticker = instrument["ticker"] 3216 self._figi = instrument["figi"] 3217 3218 if orderType == "Limit": 3219 uLogger.debug( 3220 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3221 self._ticker, self._figi, 3222 operation, lots, targetPrice, instrument["currency"], 3223 )) 3224 3225 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3226 self.body = str({ 3227 "figi": self._figi, 3228 "quantity": str(lots), 3229 "price": FloatToNano(targetPrice), 3230 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3231 "accountId": str(self.accountId), 3232 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3233 }) 3234 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3235 3236 if "orderId" in response.keys(): 3237 uLogger.info( 3238 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3239 response["orderId"], self._ticker, self._figi, operation, lots, 3240 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3241 )) 3242 3243 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3244 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3245 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3246 targetPrice, instrument["currency"], 3247 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3248 )) 3249 3250 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3251 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3252 targetPrice, instrument["currency"], 3253 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3254 )) 3255 3256 else: 3257 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3258 3259 if orderType == "Stop": 3260 uLogger.debug( 3261 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3262 self._ticker, self._figi, 3263 operation, lots, 3264 targetPrice, instrument["currency"], 3265 limitPrice, instrument["currency"], 3266 stopType, expDate, 3267 )) 3268 3269 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3270 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3271 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3272 3273 body = { 3274 "figi": self._figi, 3275 "quantity": str(lots), 3276 "price": FloatToNano(limitPrice), 3277 "stopPrice": FloatToNano(targetPrice), 3278 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3279 "accountId": str(self.accountId), 3280 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3281 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3282 } 3283 3284 if expDateUTC: 3285 body["expireDate"] = expDateUTC 3286 3287 self.body = str(body) 3288 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3289 3290 if "stopOrderId" in response.keys(): 3291 uLogger.info( 3292 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3293 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3294 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3295 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3296 TKS_STOP_ORDER_TYPES[stopOrderType], 3297 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3298 )) 3299 3300 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3301 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3302 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3303 targetPrice, instrument["currency"], 3304 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3305 )) 3306 3307 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3308 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3309 targetPrice, instrument["currency"], 3310 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3311 )) 3312 3313 else: 3314 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3315 3316 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3318 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3319 """ 3320 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3321 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3322 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3323 See also: `Order()` docstring. 3324 3325 :param lots: volume, integer count of lots >= 1. 3326 :param targetPrice: target price > 0. This is open trade price for limit order. 3327 :return: JSON with response from broker server. 3328 """ 3329 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3331 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3332 """ 3333 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3334 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3335 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3336 target price value then broker opens a limit order. See also: `Order()` docstring. 3337 3338 :param lots: volume, integer count of lots >= 1. 3339 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3340 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3341 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3342 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3343 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3344 :param expDate: string "Undefined" by default or local date in future. 3345 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3346 This date is converting to UTC format for server. 3347 :return: JSON with response from broker server. 3348 """ 3349 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3351 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3352 """ 3353 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3354 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3355 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3356 See also: `Order()` docstring. 3357 3358 :param lots: volume, integer count of lots >= 1. 3359 :param targetPrice: target price > 0. This is open trade price for limit order. 3360 :return: JSON with response from broker server. 3361 """ 3362 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3364 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3365 """ 3366 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3367 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3368 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3369 target price value then broker opens a limit order. See also: `Order()` docstring. 3370 3371 :param lots: volume, integer count of lots >= 1. 3372 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3373 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3374 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3375 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3376 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3377 :param expDate: string "Undefined" by default or local date in future. 3378 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3379 This date is converting to UTC format for server. 3380 :return: JSON with response from broker server. 3381 """ 3382 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3384 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3385 """ 3386 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3387 3388 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3389 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3390 This avoids unnecessary downloading data from the server. 3391 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3392 """ 3393 if self.accountId is None or not self.accountId: 3394 uLogger.error("Variable `accountId` must be defined for using this method!") 3395 raise Exception("Account ID required") 3396 3397 if orderIDs: 3398 if allOrdersIDs is None: 3399 rawOrders = self.RequestPendingOrders() 3400 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3401 3402 if allStopOrdersIDs is None: 3403 rawStopOrders = self.RequestStopOrders() 3404 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3405 3406 for orderID in orderIDs: 3407 idInPendingOrders = orderID in allOrdersIDs 3408 idInStopOrders = orderID in allStopOrdersIDs 3409 3410 if not (idInPendingOrders or idInStopOrders): 3411 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3412 continue 3413 3414 else: 3415 if idInPendingOrders: 3416 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3417 3418 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3419 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3420 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3421 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3422 3423 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3424 if self.moreDebug: 3425 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3426 3427 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3428 3429 else: 3430 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3431 3432 elif idInStopOrders: 3433 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3434 3435 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3436 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3437 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3438 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3439 3440 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3441 if self.moreDebug: 3442 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3443 3444 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3445 3446 else: 3447 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3448 3449 else: 3450 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3452 def CloseAllOrders(self) -> None: 3453 """ 3454 Gets a list of open pending and stop orders and cancel it all. 3455 """ 3456 rawOrders = self.RequestPendingOrders() 3457 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3458 lenOrders = len(allOrdersIDs) 3459 3460 rawStopOrders = self.RequestStopOrders() 3461 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3462 lenSOrders = len(allStopOrdersIDs) 3463 3464 if lenOrders > 0 or lenSOrders > 0: 3465 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3466 3467 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3468 3469 else: 3470 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3472 def CloseAll(self, *args) -> None: 3473 """ 3474 Close all available (not blocked) opened trades and orders. 3475 3476 Also, you can select one or more keywords case-insensitive: 3477 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3478 3479 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3480 """ 3481 overview = self.Overview(show=False) # get all open trades info 3482 3483 if len(args) == 0: 3484 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3485 self.CloseAllOrders() # close all pending and stop orders 3486 3487 for iType in TKS_INSTRUMENTS: 3488 if iType != "Currencies": 3489 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3490 3491 else: 3492 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3493 lowerArgs = [x.lower() for x in args] 3494 3495 if "orders" in lowerArgs: 3496 self.CloseAllOrders() # close all pending and stop orders 3497 3498 for iType in TKS_INSTRUMENTS: 3499 if iType.lower() in lowerArgs and iType != "Currencies": 3500 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3502 def CloseAllByTicker(self, instrument: str) -> None: 3503 """ 3504 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3505 3506 This method searches opened trade and orders of instrument throw all portfolio and then use 3507 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3508 3509 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3510 3511 :param instrument: string with ticker. 3512 """ 3513 if instrument is None or not instrument: 3514 uLogger.error("Ticker name must be defined for using this method!") 3515 raise Exception("Ticker required") 3516 3517 overview = self.Overview(show=False) # get user portfolio with all open trades info 3518 3519 self._ticker = instrument # try to set instrument as ticker 3520 self._figi = "" 3521 3522 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3523 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3524 3525 if limitAll and self.IsInLimitOrders(portfolio=overview): 3526 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3527 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3528 3529 if stopAll and self.IsInStopOrders(portfolio=overview): 3530 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3531 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3532 3533 if self.IsInPortfolio(portfolio=overview): 3534 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3535 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with ticker.
3537 def CloseAllByFIGI(self, instrument: str) -> None: 3538 """ 3539 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3540 3541 This method searches opened trade and orders of instrument throw all portfolio and then use 3542 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3543 3544 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3545 3546 :param instrument: string with FIGI id. 3547 """ 3548 if instrument is None or not instrument: 3549 uLogger.error("FIGI id must be defined for using this method!") 3550 raise Exception("FIGI required") 3551 3552 overview = self.Overview(show=False) # get user portfolio with all open trades info 3553 3554 self._ticker = "" 3555 self._figi = instrument # try to set instrument as FIGI id 3556 3557 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3558 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3559 3560 if limitAll and self.IsInLimitOrders(portfolio=overview): 3561 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3562 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3563 3564 if stopAll and self.IsInStopOrders(portfolio=overview): 3565 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3566 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3567 3568 if self.IsInPortfolio(portfolio=overview): 3569 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3570 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with FIGI id.
3572 @staticmethod 3573 def ParseOrderParameters(operation, **inputParameters): 3574 """ 3575 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3576 3577 :param operation: string "Buy" or "Sell". 3578 :param inputParameters: this is dict of strings that looks like this 3579 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3580 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3581 "prices" key: one or more prices to open limit-orders 3582 Counts of values in lots and prices lists must be equals! 3583 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3584 """ 3585 # TODO: update order grid work with api v2 3586 pass 3587 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3588 # 3589 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3590 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3591 # raise Exception("Incorrect value") 3592 # 3593 # if "l" in inputParameters.keys(): 3594 # inputParameters["lots"] = inputParameters.pop("l") 3595 # 3596 # if "p" in inputParameters.keys(): 3597 # inputParameters["prices"] = inputParameters.pop("p") 3598 # 3599 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3600 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3601 # raise Exception("Incorrect value") 3602 # 3603 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3604 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3605 # 3606 # if len(lots) != len(prices): 3607 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3608 # raise Exception("Incorrect value") 3609 # 3610 # uLogger.debug("Extracted parameters for orders:") 3611 # uLogger.debug("lots = {}".format(lots)) 3612 # uLogger.debug("prices = {}".format(prices)) 3613 # 3614 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3615 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3616 # uLogger.debug("Order parameters: {}".format(result)) 3617 # 3618 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3620 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3621 """ 3622 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3623 3624 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3625 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3626 """ 3627 result = False 3628 msg = "Instrument not defined!" 3629 3630 if portfolio is None or not portfolio: 3631 portfolio = self.Overview(show=False) 3632 3633 if self._ticker: 3634 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3635 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3636 3637 for iType in TKS_INSTRUMENTS: 3638 for instrument in portfolio["stat"][iType]: 3639 if instrument["ticker"] == self._ticker: 3640 result = True 3641 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3642 break 3643 3644 elif self._figi: 3645 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3646 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3647 3648 for iType in TKS_INSTRUMENTS: 3649 for instrument in portfolio["stat"][iType]: 3650 if instrument["figi"] == self._figi: 3651 result = True 3652 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3653 break 3654 3655 else: 3656 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3657 3658 uLogger.debug(msg) 3659 3660 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3662 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3663 """ 3664 Returns instrument from the user's portfolio if it presents there. 3665 Instrument must be defined by `ticker` (highly priority) or `figi`. 3666 3667 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3668 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3669 """ 3670 result = None 3671 msg = "Instrument not defined!" 3672 3673 if portfolio is None or not portfolio: 3674 portfolio = self.Overview(show=False) 3675 3676 if self._ticker: 3677 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3678 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3679 3680 for iType in TKS_INSTRUMENTS: 3681 for instrument in portfolio["stat"][iType]: 3682 if instrument["ticker"] == self._ticker: 3683 result = instrument 3684 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3685 break 3686 3687 elif self._figi: 3688 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3689 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3690 3691 for iType in TKS_INSTRUMENTS: 3692 for instrument in portfolio["stat"][iType]: 3693 if instrument["figi"] == self._figi: 3694 result = instrument 3695 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3696 break 3697 3698 else: 3699 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3700 3701 uLogger.debug(msg) 3702 3703 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3705 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3706 """ 3707 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3708 3709 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3710 3711 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3712 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3713 """ 3714 result = False 3715 msg = "Instrument not defined!" 3716 3717 if portfolio is None or not portfolio: 3718 portfolio = self.Overview(show=False) 3719 3720 if self._ticker: 3721 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3722 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3723 3724 for instrument in portfolio["stat"]["orders"]: 3725 if instrument["ticker"] == self._ticker: 3726 result = True 3727 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3728 break 3729 3730 elif self._figi: 3731 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3732 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3733 3734 for instrument in portfolio["stat"]["orders"]: 3735 if instrument["figi"] == self._figi: 3736 result = True 3737 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3738 break 3739 3740 else: 3741 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3742 3743 uLogger.debug(msg) 3744 3745 return result
Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif limit orders list contains some limit orders for the instrument,Falseotherwise.
3747 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3748 """ 3749 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3750 Instrument must be defined by `ticker` (highly priority) or `figi`. 3751 3752 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3753 3754 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3755 :return: list with `orderID`s of limit orders. 3756 """ 3757 result = [] 3758 msg = "Instrument not defined!" 3759 3760 if portfolio is None or not portfolio: 3761 portfolio = self.Overview(show=False) 3762 3763 if self._ticker: 3764 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3765 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3766 3767 for instrument in portfolio["stat"]["orders"]: 3768 if instrument["ticker"] == self._ticker: 3769 result.append(instrument["orderID"]) 3770 3771 if result: 3772 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3773 3774 elif self._figi: 3775 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3776 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3777 3778 for instrument in portfolio["stat"]["orders"]: 3779 if instrument["figi"] == self._figi: 3780 result.append(instrument["orderID"]) 3781 3782 if result: 3783 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3784 3785 else: 3786 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3787 3788 uLogger.debug(msg) 3789 3790 return result
Returns list with all orderIDs of opened pending limit orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of limit orders.
3792 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3793 """ 3794 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3795 3796 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3797 3798 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3799 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3800 """ 3801 result = False 3802 msg = "Instrument not defined!" 3803 3804 if portfolio is None or not portfolio: 3805 portfolio = self.Overview(show=False) 3806 3807 if self._ticker: 3808 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3809 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3810 3811 for instrument in portfolio["stat"]["stopOrders"]: 3812 if instrument["ticker"] == self._ticker: 3813 result = True 3814 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3815 break 3816 3817 elif self._figi: 3818 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3819 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3820 3821 for instrument in portfolio["stat"]["stopOrders"]: 3822 if instrument["figi"] == self._figi: 3823 result = True 3824 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3825 break 3826 3827 else: 3828 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3829 3830 uLogger.debug(msg) 3831 3832 return result
Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif stop orders list contains some stop orders for the instrument,Falseotherwise.
3834 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3835 """ 3836 Returns list with all `orderID`s of opened stop orders for the instrument. 3837 Instrument must be defined by `ticker` (highly priority) or `figi`. 3838 3839 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3840 3841 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3842 :return: list with `orderID`s of stop orders. 3843 """ 3844 result = [] 3845 msg = "Instrument not defined!" 3846 3847 if portfolio is None or not portfolio: 3848 portfolio = self.Overview(show=False) 3849 3850 if self._ticker: 3851 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3852 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3853 3854 for instrument in portfolio["stat"]["stopOrders"]: 3855 if instrument["ticker"] == self._ticker: 3856 result.append(instrument["orderID"]) 3857 3858 if result: 3859 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3860 3861 elif self._figi: 3862 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3863 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3864 3865 for instrument in portfolio["stat"]["stopOrders"]: 3866 if instrument["figi"] == self._figi: 3867 result.append(instrument["orderID"]) 3868 3869 if result: 3870 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3871 3872 else: 3873 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3874 3875 uLogger.debug(msg) 3876 3877 return result
Returns list with all orderIDs of opened stop orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of stop orders.
3879 def RequestLimits(self) -> dict: 3880 """ 3881 Method for obtaining the available funds for withdrawal for current `accountId`. 3882 3883 See also: 3884 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3885 - `OverviewLimits()` method 3886 3887 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3888 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3889 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3890 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3891 """ 3892 if self.accountId is None or not self.accountId: 3893 uLogger.error("Variable `accountId` must be defined for using this method!") 3894 raise Exception("Account ID required") 3895 3896 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3897 3898 self.body = str({"accountId": self.accountId}) 3899 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3900 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3901 3902 if self.moreDebug: 3903 uLogger.debug("Records about available funds for withdrawal successfully received") 3904 3905 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3907 def OverviewLimits(self, show: bool = False) -> dict: 3908 """ 3909 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3910 3911 See also: `RequestLimits()`. 3912 3913 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3914 :return: dict with raw parsed data from server and some calculated statistics about it. 3915 """ 3916 if self.accountId is None or not self.accountId: 3917 uLogger.error("Variable `accountId` must be defined for using this method!") 3918 raise Exception("Account ID required") 3919 3920 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3921 3922 view = { 3923 "rawLimits": rawLimits, 3924 "limits": { # parsed data for every currency: 3925 "money": { # this is an array of portfolio currency positions 3926 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3927 }, 3928 "blocked": { # this is an array of blocked currency 3929 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3930 }, 3931 "blockedGuarantee": { # this is locked money under collateral for futures 3932 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3933 }, 3934 }, 3935 } 3936 3937 # --- Prepare text table with limits in human-readable format: 3938 if show: 3939 info = [ 3940 "# Withdrawal limits\n\n", 3941 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3942 "* **Account ID:** [{}]\n".format(self.accountId), 3943 ] 3944 3945 if view["limits"]["money"]: 3946 info.extend([ 3947 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3948 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3949 ]) 3950 3951 else: 3952 info.append("\nNo withdrawal limits\n") 3953 3954 for curr in view["limits"]["money"].keys(): 3955 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3956 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3957 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3958 3959 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3960 "[{}]".format(curr), 3961 "{:.2f}".format(view["limits"]["money"][curr]), 3962 "{:.2f}".format(availableMoney), 3963 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3964 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3965 ) 3966 3967 if curr == "rub": 3968 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3969 3970 else: 3971 info.append(infoStr) 3972 3973 infoText = "".join(info) 3974 3975 uLogger.info(infoText) 3976 3977 if self.withdrawalLimitsFile: 3978 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3979 fH.write(infoText) 3980 3981 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3982 3983 if self.useHTMLReports: 3984 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3985 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3986 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3987 3988 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3989 3990 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3992 def RequestAccounts(self) -> dict: 3993 """ 3994 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3995 3996 See also: 3997 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3998 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3999 - `OverviewUserInfo()` method 4000 4001 :return: dict with raw data from server that contains accounts info. Example of dict: 4002 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4003 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4004 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4005 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4006 """ 4007 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4008 4009 self.body = str({}) 4010 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4011 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4012 4013 if self.moreDebug: 4014 uLogger.debug("Records about available accounts successfully received") 4015 4016 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
4018 def RequestUserInfo(self) -> dict: 4019 """ 4020 Method for requesting common user's information. 4021 4022 See also: 4023 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4024 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4025 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4026 - `OverviewUserInfo()` method 4027 4028 :return: dict with raw data from server that contains user's information. Example of dict: 4029 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4030 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4031 """ 4032 uLogger.debug("Requesting common user's information. Wait, please...") 4033 4034 self.body = str({}) 4035 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4036 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4037 4038 if self.moreDebug: 4039 uLogger.debug("Records about current user successfully received") 4040 4041 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
4043 def RequestMarginStatus(self, accountId: str = None) -> dict: 4044 """ 4045 Method for requesting margin calculation for defined account ID. 4046 4047 See also: 4048 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4049 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4050 - `OverviewUserInfo()` method 4051 4052 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4053 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4054 Example of responses: 4055 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4056 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4057 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4058 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4059 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4060 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4061 """ 4062 if accountId is None or not accountId: 4063 if self.accountId is None or not self.accountId: 4064 uLogger.error("Variable `accountId` must be defined for using this method!") 4065 raise Exception("Account ID required") 4066 4067 else: 4068 accountId = self.accountId # use `self.accountId` (main ID) by default 4069 4070 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4071 4072 self.body = str({"accountId": accountId}) 4073 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4074 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4075 4076 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4077 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4078 rawMargin = {} 4079 4080 else: 4081 if self.moreDebug: 4082 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4083 4084 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
4086 def RequestTariffLimits(self) -> dict: 4087 """ 4088 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4089 4090 See also: 4091 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4092 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4093 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4094 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4095 - `OverviewUserInfo()` method 4096 4097 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4098 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4099 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4100 """ 4101 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4102 4103 self.body = str({}) 4104 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4105 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4106 4107 if self.moreDebug: 4108 uLogger.debug("Records with limits of current tariff successfully received") 4109 4110 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
4112 def RequestBondCoupons(self, iJSON: dict) -> dict: 4113 """ 4114 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4115 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4116 All dates are in UTC timezone. 4117 4118 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4119 Documentation: 4120 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4121 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4122 4123 See also: `ExtendBondsData()`. 4124 4125 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4126 If raw iJSON is not data of bond then server returns an error [400] with message: 4127 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4128 :return: dictionary with bond payment calendar. Response example 4129 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4130 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4131 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4132 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4133 """ 4134 if iJSON["figi"] is None or not iJSON["figi"]: 4135 uLogger.error("FIGI must be defined for using this method!") 4136 raise Exception("FIGI required") 4137 4138 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4139 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4140 4141 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4142 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4143 self._figi, 4144 startDate, 4145 endDate, 4146 )) 4147 4148 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4149 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4150 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4151 4152 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4153 uLogger.warning("Instrument type is not bond!") 4154 4155 else: 4156 if self.moreDebug: 4157 uLogger.debug("Records about bond payment calendar successfully received") 4158 4159 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self._ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
4161 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4162 """ 4163 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4164 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4165 coupon yields, current yields and some statistics etc. 4166 4167 WARNING! This is too long operation if a lot of bonds requested from broker server. 4168 4169 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4170 4171 :param instruments: list of strings with tickers or FIGIs. 4172 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4173 for further used by data scientists or stock analytics. 4174 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4175 In XLSX-file and Pandas DataFrame fields mean: 4176 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4177 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4178 """ 4179 if instruments is None or not instruments: 4180 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4181 raise Exception("Ticker or FIGI required") 4182 4183 if isinstance(instruments, str): 4184 instruments = [instruments] 4185 4186 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4187 4188 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4189 4190 iCount = len(uniqueInstruments) 4191 tooLong = iCount >= 20 4192 if tooLong: 4193 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4194 4195 bonds = None 4196 for i, self._figi in enumerate(uniqueInstruments): 4197 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4198 4199 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4200 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4201 rawBond = self.SearchByFIGI(requestPrice=True) 4202 4203 # Widen raw data with UTC current time (iData["actualDateTime"]): 4204 actualDate = datetime.now(tzutc()) 4205 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4206 4207 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4208 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4209 4210 # Replace some values with human-readable: 4211 iData["nominalCurrency"] = iData["nominal"]["currency"] 4212 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4213 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4214 iData["aciCurrency"] = iData["aciValue"]["currency"] 4215 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4216 iData["issueSize"] = int(iData["issueSize"]) 4217 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4218 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4219 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4220 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4221 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4222 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4223 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4224 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4225 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4226 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4227 4228 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4229 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4230 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4231 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4232 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4233 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4234 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4235 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4236 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4237 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4238 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4239 4240 # Widen raw data with calendar data from `rawCalendar` values: 4241 calendarData = [] 4242 if "events" in iData["rawCalendar"].keys(): 4243 for item in iData["rawCalendar"]["events"]: 4244 calendarData.append({ 4245 "couponDate": item["couponDate"], 4246 "couponNumber": int(item["couponNumber"]), 4247 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4248 "payCurrency": item["payOneBond"]["currency"], 4249 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4250 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4251 "couponStartDate": item["couponStartDate"], 4252 "couponEndDate": item["couponEndDate"], 4253 "couponPeriod": item["couponPeriod"], 4254 }) 4255 4256 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4257 if "maturityDate" not in iData.keys(): 4258 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4259 4260 # Widen raw data with Coupon Rate. 4261 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4262 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4263 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4264 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4265 4266 # Widen raw data with Yield to Maturity (YTM) on current date. 4267 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4268 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4269 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4270 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4271 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4272 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4273 4274 iData["calendar"] = calendarData # adds calendar at the end 4275 4276 # Remove not used data: 4277 iData.pop("uid") 4278 iData.pop("positionUid") 4279 iData.pop("currentPrice") 4280 iData.pop("rawCalendar") 4281 4282 colNames = list(iData.keys()) 4283 if bonds is None: 4284 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4285 4286 else: 4287 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4288 4289 else: 4290 uLogger.warning("Instrument is not a bond!") 4291 4292 processed = round(100 * (i + 1) / iCount, 1) 4293 if tooLong and processed % 5 == 0: 4294 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4295 4296 else: 4297 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4298 4299 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4300 4301 # Saving bonds from Pandas DataFrame to XLSX sheet: 4302 if xlsx and self.bondsXLSXFile: 4303 with pd.ExcelWriter( 4304 path=self.bondsXLSXFile, 4305 date_format=TKS_DATE_FORMAT, 4306 datetime_format=TKS_DATE_TIME_FORMAT, 4307 mode="w", 4308 ) as writer: 4309 bonds.to_excel( 4310 writer, 4311 sheet_name="Extended bonds data", 4312 index=True, 4313 encoding="UTF-8", 4314 freeze_panes=(1, 1), 4315 ) # saving as XLSX-file with freeze first row and column as headers 4316 4317 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4318 4319 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4321 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4322 """ 4323 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4324 4325 WARNING! This is too long operation if a lot of bonds requested from broker server. 4326 4327 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4328 4329 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4330 extended information about bonds: main info, current prices, bond payment calendar, 4331 coupon yields, current yields and some statistics etc. 4332 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4333 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4334 for further used by data scientists or stock analytics. 4335 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4336 """ 4337 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4338 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4339 4340 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4341 4342 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4343 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4344 calendar = None 4345 for bond in extBonds.iterrows(): 4346 for item in bond[1]["calendar"]: 4347 cData = { 4348 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4349 "couponDate": item["couponDate"], 4350 "figi": bond[1]["figi"], 4351 "ticker": bond[1]["ticker"], 4352 "name": bond[1]["name"], 4353 "couponNumber": item["couponNumber"], 4354 "payOneBond": item["payOneBond"], 4355 "payCurrency": item["payCurrency"], 4356 "couponType": item["couponType"], 4357 "couponPeriod": item["couponPeriod"], 4358 "fixDate": item["fixDate"], 4359 "couponStartDate": item["couponStartDate"], 4360 "couponEndDate": item["couponEndDate"], 4361 } 4362 4363 if calendar is None: 4364 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4365 4366 else: 4367 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4368 4369 if calendar is not None: 4370 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4371 4372 # Saving calendar from Pandas DataFrame to XLSX sheet: 4373 if xlsx: 4374 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4375 4376 with pd.ExcelWriter( 4377 path=xlsxCalendarFile, 4378 date_format=TKS_DATE_FORMAT, 4379 datetime_format=TKS_DATE_TIME_FORMAT, 4380 mode="w", 4381 ) as writer: 4382 humanReadable = calendar.copy(deep=True) 4383 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4384 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4385 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4386 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4387 humanReadable.columns = colNames # human-readable column names 4388 4389 humanReadable.to_excel( 4390 writer, 4391 sheet_name="Bond payments calendar", 4392 index=False, 4393 encoding="UTF-8", 4394 freeze_panes=(1, 2), 4395 ) # saving as XLSX-file with freeze first row and column as headers 4396 4397 del humanReadable # release df in memory 4398 4399 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4400 4401 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4403 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4404 """ 4405 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4406 Also, creates Markdown file with calendar data, `calendar.md` by default. 4407 4408 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4409 4410 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4411 extended information about bonds: main info, current prices, bond payment calendar, 4412 coupon yields, current yields and some statistics etc. 4413 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4414 :param show: if `True` then also printing bonds payment calendar to the console, 4415 otherwise save to file `calendarFile` only. `False` by default. 4416 :return: multilines text in Markdown format with bonds payment calendar as a table. 4417 """ 4418 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4419 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4420 4421 infoText = "# Bond payments calendar\n\n" 4422 4423 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4424 4425 if not (calendar is None or calendar.empty): 4426 splitLine = "| | | | | | | | | |\n" 4427 4428 info = [ 4429 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4430 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4431 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4432 ] 4433 4434 newMonth = False 4435 notOneBond = calendar["figi"].nunique() > 1 4436 for i, bond in enumerate(calendar.iterrows()): 4437 if newMonth and notOneBond: 4438 info.append(splitLine) 4439 4440 info.append( 4441 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4442 " √" if bond[1]["paid"] else " —", 4443 bond[1]["couponDate"].split("T")[0], 4444 bond[1]["figi"], 4445 bond[1]["ticker"], 4446 bond[1]["couponNumber"], 4447 "{} {}".format( 4448 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4449 bond[1]["payCurrency"], 4450 ), 4451 bond[1]["couponType"], 4452 bond[1]["couponPeriod"], 4453 bond[1]["fixDate"].split("T")[0], 4454 ) 4455 ) 4456 4457 if i < len(calendar.values) - 1: 4458 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4459 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4460 newMonth = False if curDate.month == nextDate.month else True 4461 4462 else: 4463 newMonth = False 4464 4465 infoText += "".join(info) 4466 4467 if show: 4468 uLogger.info("{}".format(infoText)) 4469 4470 if self.calendarFile is not None: 4471 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4472 fH.write(infoText) 4473 4474 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4475 4476 if self.useHTMLReports: 4477 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4478 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4479 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4480 4481 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4482 4483 else: 4484 infoText += "No data\n" 4485 4486 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4488 def OverviewAccounts(self, show: bool = False) -> dict: 4489 """ 4490 Method for parsing and show simple table with all available user accounts. 4491 4492 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4493 4494 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4495 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4496 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4497 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4498 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4499 "closed": "—", "access": "Full access" }, ...}}` 4500 """ 4501 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4502 4503 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4504 accounts = { 4505 item["id"]: { 4506 "type": TKS_ACCOUNT_TYPES[item["type"]], 4507 "name": item["name"], 4508 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4509 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4510 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4511 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4512 } for item in rawAccounts["accounts"] 4513 } 4514 4515 # Raw and parsed data with some fields replaced in "stat" section: 4516 view = { 4517 "rawAccounts": rawAccounts, 4518 "stat": accounts, 4519 } 4520 4521 # --- Prepare simple text table with only accounts data in human-readable format: 4522 if show: 4523 info = [ 4524 "# User accounts\n\n", 4525 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4526 "| Account ID | Type | Status | Name |\n", 4527 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4528 ] 4529 4530 for account in view["stat"].keys(): 4531 info.extend([ 4532 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4533 account, 4534 view["stat"][account]["type"], 4535 view["stat"][account]["status"], 4536 view["stat"][account]["name"], 4537 ) 4538 ]) 4539 4540 infoText = "".join(info) 4541 4542 uLogger.info(infoText) 4543 4544 if self.userAccountsFile: 4545 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4546 fH.write(infoText) 4547 4548 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4549 4550 if self.useHTMLReports: 4551 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4552 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4553 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4554 4555 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4556 4557 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4559 def OverviewUserInfo(self, show: bool = False) -> dict: 4560 """ 4561 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4562 4563 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4564 4565 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4566 :return: dict with raw parsed data from server and some calculated statistics about it. 4567 """ 4568 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4569 tmpTicker = self._ticker 4570 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4571 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4572 self._ticker = tmpTicker 4573 4574 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4575 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4576 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4577 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4578 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4579 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4580 4581 # This is dict with parsed common user data: 4582 userInfo = { 4583 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4584 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4585 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4586 "tariff": rawUserInfo["tariff"], 4587 } 4588 4589 # This is an array of dict with parsed margin statuses for every account IDs: 4590 margins = {} 4591 for accountId in accounts.keys(): 4592 if rawMargins[accountId]: 4593 margins[accountId] = { 4594 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4595 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4596 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4597 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4598 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4599 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4600 "missing": missing["volume"], 4601 } 4602 4603 else: 4604 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4605 4606 unary = {} # unary-connection limits 4607 for item in rawTariffLimits["unaryLimits"]: 4608 if item["limitPerMinute"] in unary.keys(): 4609 unary[item["limitPerMinute"]].extend(item["methods"]) 4610 4611 else: 4612 unary[item["limitPerMinute"]] = item["methods"] 4613 4614 stream = {} # stream-connection limits 4615 for item in rawTariffLimits["streamLimits"]: 4616 if item["limit"] in stream.keys(): 4617 stream[item["limit"]].extend(item["streams"]) 4618 4619 else: 4620 stream[item["limit"]] = item["streams"] 4621 4622 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4623 limits = { 4624 "unary": unary, 4625 "stream": stream, 4626 } 4627 4628 # Raw and parsed data as an output result: 4629 view = { 4630 "rawUserInfo": rawUserInfo, 4631 "rawAccounts": rawAccounts, 4632 "rawMargins": rawMargins, 4633 "rawTariffLimits": rawTariffLimits, 4634 "stat": { 4635 "overview": overview, 4636 "userInfo": userInfo, 4637 "accounts": accounts, 4638 "margins": margins, 4639 "limits": limits, 4640 }, 4641 } 4642 4643 # --- Prepare text table with user information in human-readable format: 4644 if show: 4645 info = [ 4646 "# Full user information\n\n", 4647 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4648 "## Common information\n\n", 4649 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4650 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4651 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4652 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4653 "\n## User accounts\n\n", 4654 ] 4655 4656 for account in view["stat"]["accounts"].keys(): 4657 info.extend([ 4658 "### ID: [{}]\n\n".format(account), 4659 "| Parameters | Values |\n", 4660 "|----------------------|--------------------------------------------------------------|\n", 4661 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4662 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4663 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4664 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4665 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4666 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4667 ]) 4668 4669 if margins[account]: 4670 info.extend([ 4671 "| Margin status: | Enabled |\n", 4672 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4673 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4674 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4675 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4676 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4677 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4678 ]) 4679 4680 else: 4681 info.append("| Margin status: | Disabled |\n\n") 4682 4683 info.extend([ 4684 "\n## Current user tariff limits\n", 4685 "\n### See also\n", 4686 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4687 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4688 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4689 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4690 "\n### Unary limits\n", 4691 ]) 4692 4693 if unary: 4694 for key, values in sorted(unary.items()): 4695 info.append("\n* Max requests per minute: {}\n".format(key)) 4696 4697 for value in values: 4698 info.append(" - {}\n".format(value)) 4699 4700 else: 4701 info.append("\nNot available\n") 4702 4703 info.append("\n### Stream limits\n") 4704 4705 if stream: 4706 for key, values in sorted(stream.items()): 4707 info.append("\n* Max stream connections: {}\n".format(key)) 4708 4709 for value in values: 4710 info.append(" - {}\n".format(value)) 4711 4712 else: 4713 info.append("\nNot available\n") 4714 4715 infoText = "".join(info) 4716 4717 uLogger.info(infoText) 4718 4719 if self.userInfoFile: 4720 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4721 fH.write(infoText) 4722 4723 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4724 4725 if self.useHTMLReports: 4726 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4727 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4728 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4729 4730 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4731 4732 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4735class Args: 4736 """ 4737 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4738 """ 4739 def __init__(self, **kwargs): 4740 self.__dict__.update(kwargs) 4741 4742 def __getattr__(self, item): 4743 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4746def ParseArgs(): 4747 """This function get and parse command line keys.""" 4748 parser = ArgumentParser() # command-line string parser 4749 4750 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4751 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4752 4753 # --- options: 4754 4755 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4756 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4757 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4758 4759 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4760 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4761 4762 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4763 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4764 4765 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4766 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4767 4768 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4769 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4770 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4771 4772 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4773 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4774 4775 # --- commands: 4776 4777 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4778 4779 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4780 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4781 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4782 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4783 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4784 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4785 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4786 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4787 4788 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4789 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4790 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4791 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4792 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4793 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4794 4795 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4796 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4797 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4798 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4799 4800 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4801 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4802 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4803 4804 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4805 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4806 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4807 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4808 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4809 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4810 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4811 4812 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4813 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4814 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4815 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4816 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4817 4818 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4819 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4820 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4821 4822 cmdArgs = parser.parse_args() 4823 return cmdArgs
This function get and parse command line keys.
4826def Main(**kwargs): 4827 """ 4828 Main function for work with TKSBrokerAPI in the console. 4829 4830 See examples: 4831 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4832 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4833 """ 4834 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4835 4836 if args.debug_level: 4837 uLogger.level = 10 # always debug level by default 4838 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4839 4840 exitCode = 0 4841 start = datetime.now(tzutc()) 4842 uLogger.debug("=-" * 50) 4843 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4844 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4845 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4846 )) 4847 4848 # trying to calculate full current version: 4849 buildVersion = __version__ 4850 try: 4851 v = version("tksbrokerapi") 4852 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4853 4854 except Exception: 4855 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4856 4857 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4858 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4859 4860 try: 4861 if args.version: 4862 print("TKSBrokerAPI {}".format(buildVersion)) 4863 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4864 4865 else: 4866 # Init class for trading with Tinkoff Broker: 4867 trader = TinkoffBrokerServer( 4868 token=args.token, 4869 accountId=args.account_id, 4870 useCache=not args.no_cache, 4871 ) 4872 4873 # --- set some options: 4874 4875 if args.more: 4876 trader.moreDebug = True 4877 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4878 4879 if args.html: 4880 trader.useHTMLReports = True 4881 4882 if args.ticker: 4883 ticker = str(args.ticker).upper() # Tickers may be upper case only 4884 4885 if ticker in trader.aliasesKeys: 4886 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4887 4888 else: 4889 trader.ticker = ticker 4890 4891 if args.figi: 4892 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4893 4894 if args.depth is not None: 4895 trader.depth = args.depth 4896 4897 # --- do one command: 4898 4899 if args.list: 4900 if args.output is not None: 4901 trader.instrumentsFile = args.output 4902 4903 trader.ShowInstrumentsInfo(show=True) 4904 4905 elif args.list_xlsx: 4906 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4907 4908 elif args.bonds_xlsx is not None: 4909 if args.output is not None: 4910 trader.bondsXLSXFile = args.output 4911 4912 if len(args.bonds_xlsx) == 0: 4913 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4914 4915 else: 4916 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4917 4918 elif args.search: 4919 if args.output is not None: 4920 trader.searchResultsFile = args.output 4921 4922 trader.SearchInstruments(pattern=args.search[0], show=True) 4923 4924 elif args.info: 4925 if not (args.ticker or args.figi): 4926 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4927 raise Exception("Ticker or FIGI required") 4928 4929 if args.output is not None: 4930 trader.infoFile = args.output 4931 4932 if args.ticker: 4933 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4934 4935 else: 4936 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4937 4938 elif args.calendar is not None: 4939 if args.output is not None: 4940 trader.calendarFile = args.output 4941 4942 if len(args.calendar) == 0: 4943 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4944 4945 else: 4946 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4947 4948 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4949 4950 elif args.price: 4951 if not (args.ticker or args.figi): 4952 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4953 raise Exception("Ticker or FIGI required") 4954 4955 trader.GetCurrentPrices(show=True) 4956 4957 elif args.prices is not None: 4958 if args.output is not None: 4959 trader.pricesFile = args.output 4960 4961 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4962 4963 elif args.overview: 4964 if args.output is not None: 4965 trader.overviewFile = args.output 4966 4967 trader.Overview(show=True, details="full") 4968 4969 elif args.overview_digest: 4970 if args.output is not None: 4971 trader.overviewDigestFile = args.output 4972 4973 trader.Overview(show=True, details="digest") 4974 4975 elif args.overview_positions: 4976 if args.output is not None: 4977 trader.overviewPositionsFile = args.output 4978 4979 trader.Overview(show=True, details="positions") 4980 4981 elif args.overview_orders: 4982 if args.output is not None: 4983 trader.overviewOrdersFile = args.output 4984 4985 trader.Overview(show=True, details="orders") 4986 4987 elif args.overview_analytics: 4988 if args.output is not None: 4989 trader.overviewAnalyticsFile = args.output 4990 4991 trader.Overview(show=True, details="analytics") 4992 4993 elif args.overview_calendar: 4994 if args.output is not None: 4995 trader.overviewAnalyticsFile = args.output 4996 4997 trader.Overview(show=True, details="calendar") 4998 4999 elif args.deals is not None: 5000 if args.output is not None: 5001 trader.reportFile = args.output 5002 5003 if 0 <= len(args.deals) < 3: 5004 trader.Deals( 5005 start=args.deals[0] if len(args.deals) >= 1 else None, 5006 end=args.deals[1] if len(args.deals) == 2 else None, 5007 show=True, # Always show deals report in console 5008 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5009 ) 5010 5011 else: 5012 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5013 raise Exception("Incorrect value") 5014 5015 elif args.history is not None: 5016 if args.output is not None: 5017 trader.historyFile = args.output 5018 5019 if 0 <= len(args.history) < 3: 5020 dataReceived = trader.History( 5021 start=args.history[0] if len(args.history) >= 1 else None, 5022 end=args.history[1] if len(args.history) == 2 else None, 5023 interval="hour" if args.interval is None or not args.interval else args.interval, 5024 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5025 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5026 show=True, # shows all downloaded candles in console 5027 ) 5028 5029 if args.render_chart is not None and dataReceived is not None: 5030 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5031 5032 trader.ShowHistoryChart( 5033 candles=dataReceived, 5034 interact=iChart, 5035 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5036 ) 5037 5038 else: 5039 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5040 raise Exception("Incorrect value") 5041 5042 elif args.load_history is not None: 5043 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5044 5045 if args.render_chart is not None and histData is not None: 5046 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5047 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5048 5049 trader.ShowHistoryChart( 5050 candles=histData, 5051 interact=iChart, 5052 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5053 ) 5054 5055 elif args.trade is not None: 5056 if 1 <= len(args.trade) <= 5: 5057 trader.Trade( 5058 operation=args.trade[0], 5059 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5060 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5061 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5062 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5063 ) 5064 5065 else: 5066 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5067 5068 elif args.buy is not None: 5069 if 0 <= len(args.buy) <= 4: 5070 trader.Buy( 5071 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5072 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5073 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5074 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5075 ) 5076 5077 else: 5078 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5079 5080 elif args.sell is not None: 5081 if 0 <= len(args.sell) <= 4: 5082 trader.Sell( 5083 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5084 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5085 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5086 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5087 ) 5088 5089 else: 5090 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5091 5092 elif args.order: 5093 if 4 <= len(args.order) <= 7: 5094 trader.Order( 5095 operation=args.order[0], 5096 orderType=args.order[1], 5097 lots=int(args.order[2]), 5098 targetPrice=float(args.order[3]), 5099 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5100 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5101 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5102 ) 5103 5104 else: 5105 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5106 5107 elif args.buy_limit: 5108 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5109 5110 elif args.sell_limit: 5111 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5112 5113 elif args.buy_stop: 5114 if 2 <= len(args.buy_stop) <= 7: 5115 trader.BuyStop( 5116 lots=int(args.buy_stop[0]), 5117 targetPrice=float(args.buy_stop[1]), 5118 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5119 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5120 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5121 ) 5122 5123 else: 5124 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5125 5126 elif args.sell_stop: 5127 if 2 <= len(args.sell_stop) <= 7: 5128 trader.SellStop( 5129 lots=int(args.sell_stop[0]), 5130 targetPrice=float(args.sell_stop[1]), 5131 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5132 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5133 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5134 ) 5135 5136 else: 5137 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5138 5139 # elif args.buy_order_grid is not None: 5140 # # update order grid work with api v2 5141 # if len(args.buy_order_grid) == 2: 5142 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5143 # 5144 # for order in orderParams: 5145 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5146 # 5147 # else: 5148 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5149 # 5150 # elif args.sell_order_grid is not None: 5151 # # update order grid work with api v2 5152 # if len(args.sell_order_grid) >= 2: 5153 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5154 # 5155 # for order in orderParams: 5156 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5157 # 5158 # else: 5159 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5160 5161 elif args.close_order is not None: 5162 trader.CloseOrders(args.close_order) # close only one order 5163 5164 elif args.close_orders is not None: 5165 trader.CloseOrders(args.close_orders) # close list of orders 5166 5167 elif args.close_trade: 5168 if not (args.ticker or args.figi): 5169 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5170 raise Exception("Ticker or FIGI required") 5171 5172 if args.ticker: 5173 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5174 5175 else: 5176 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5177 5178 elif args.close_trades is not None: 5179 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5180 5181 elif args.close_all is not None: 5182 if args.ticker: 5183 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5184 5185 elif args.figi: 5186 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5187 5188 else: 5189 trader.CloseAll(*args.close_all) 5190 5191 elif args.limits: 5192 if args.output is not None: 5193 trader.withdrawalLimitsFile = args.output 5194 5195 trader.OverviewLimits(show=True) 5196 5197 elif args.user_info: 5198 if args.output is not None: 5199 trader.userInfoFile = args.output 5200 5201 trader.OverviewUserInfo(show=True) 5202 5203 elif args.account: 5204 if args.output is not None: 5205 trader.userAccountsFile = args.output 5206 5207 trader.OverviewAccounts(show=True) 5208 5209 else: 5210 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5211 raise Exception("There is no command to execute") 5212 5213 except Exception: 5214 trace = tb.format_exc() 5215 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5216 if e in trace: 5217 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5218 break 5219 5220 uLogger.debug(trace) 5221 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5222 exitCode = 255 # an error occurred, must be open a ticket for this issue 5223 5224 finally: 5225 finish = datetime.now(tzutc()) 5226 5227 if exitCode == 0: 5228 if args.more: 5229 uLogger.debug("All operations were finished success (summary code is 0).") 5230 5231 else: 5232 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5233 os.path.abspath(uLog.defaultLogFile), exitCode, 5234 )) 5235 5236 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5237 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5238 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5239 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5240 )) 5241 uLogger.debug("=-" * 50) 5242 5243 if not kwargs: 5244 sys.exit(exitCode) 5245 5246 else: 5247 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: